diff --git a/dev-packages/README.md b/dev-packages/README.md new file mode 100644 index 0000000..3179b09 --- /dev/null +++ b/dev-packages/README.md @@ -0,0 +1,22 @@ +# Eclipse GLSP - Dev Packages [![build-status](https://img.shields.io/jenkins/build?jobUrl=https://ci.eclipse.org/glsp/job/eclipse-glsp/job/glsp/job/master)](https://ci.eclipse.org/glsp/job/eclipse-glsp/job/glsp-client/job/master) + +Common shared development packages for Eclipse GLSP components that are implemented with Typescript. + +## Components + +- [`@eclipse-glsp/cli`](./cli/README.md): Provides helpful scrips and commands for developing glsp components as well as release engineering. +- [`@eclipse-glsp/config`](./config/README.md): Provides a meta package that exports common configuration objects for: + - [Typescript](https://www.typescriptlang.org/) (`tsconfig.json`) + - [ESLint](https://eslint.org/) (`.eslintrc`) + - [Prettier](https://prettier.io/) (`.prettierrc`). +- [`@eclipse-glsp/config-test`](./config-test//README.md): Provides a meta package that exports common test configuration object on top of `@eclipse-glsp/config`: + - [Mocha](https://www.npmjs.com/package/@eclipse-glsp/mocha-config) (`.mocharc`) + - [NYC](https://www.npmjs.com/package/@eclipse-glsp/nyc-config): (`.nycrc`) +- [`@eclipse-glsp/dev`](./dev//README.md): Provides an all-in-one meta package that includes `@eclipse-glsp/cli`, `@eclipse-glsp/config` and `@eclipse-glsp/config-dev` + +The packages are available via npm and are used by all GLSP components implemented with Typescript. + +## More information + +For more information, please visit the [Eclipse GLSP Umbrella repository](https://github.com/eclipse-glsp/glsp) and the [Eclipse GLSP Website](https://www.eclipse.org/glsp/). +If you have questions, please raise them in the [discussions](https://github.com/eclipse-glsp/glsp/discussions) and have a look at our [communication and support options](https://www.eclipse.org/glsp/contact/). diff --git a/dev-packages/cli/README.md b/dev-packages/cli/README.md new file mode 100644 index 0000000..d0791c4 --- /dev/null +++ b/dev-packages/cli/README.md @@ -0,0 +1,491 @@ +# Eclipse GLSP - CLI + +The `@eclipse-glsp/cli` package provides helpful scripts and commands for extension and application development. +The contributed `glsp`, is a command line tool that offers all contributed commands. + +## Getting Started + +Install `@eclipse-glsp/cli` as a dev dependency in your application. + +```bash +pnpm add --save-dev @eclipse-glsp/cli +``` + +## Usage + +```console +Usage: glsp [options] [command] + +Options: + -V, --version output the version number + -h, --help display help for command + +Commands: + checkHeaders [options] Validates the copyright year range (end year) of license header files + updateNext|u [options] [rootDir] Updates all `next` dependencies in GLSP project to the latest version + generateIndex [options] Generate index files in a given source directory. + releng Commands for GLSP release engineering (Linux only, intended for CI/Maintainer use). + repo Multi-repository management for GLSP projects + help [command] display help for command +``` + +## checkHeaders + +The `checkHeaders` command can be used to validate the copyright year (range) of license headers. +It checks for each file (matching the include pattern) whether the defined copyright range is in line with the first and last modification date in the git repository. +Found violations are printed to the console and can be fixed automatically. +The validation check can be restricted to pending changes and/or the last commit e.g. to validate a commit before creating a PR. + +```console +$ glsp checkHeaders -h + +Usage: glsp checkHeaders [options] + +Validates the copyright year range (end year) of license header files + +Arguments: + rootDir The starting directory for the check + +Options: + -t, --type The scope of the check. In addition to a full recursive check, is also possible to only + consider pending changes or the last commit (choices: "full", "changes", "lastCommit", + default: "full") + -f, --fileExtensions File extensions that should be checked (default: ["ts","tsx"]) + -e, --exclude File patterns that should be excluded from the check. New exclude patterns are added to + the default patterns (default: [**/@(node_modules|lib|dist|bundle)/**]) + --no-exclude-defaults Disables the default excludes patterns. Only explicitly passed exclude patterns (-e, + --exclude) are considered + -j, --json Also persist validation results as json file (default: false) + -a, --autoFix Auto apply & commit fixes without prompting the user (default: false) + -h, --help display help for command +``` + +## updateNext + +```console +$ glsp updateNext -h +Usage: glsp updateNext|u [options] [rootDir] + +Updates all `next` dependencies in GLSP project to the latest version + +Arguments: + rootDir The repository root (default: "") + +Options: + -v, --verbose Enable verbose (debug) log output (default: false) + -h, --help display help for command +``` + +## generateIndex + +Use this command to create an index file of all sources for a given directory and all it's sub directories. + +```console +$ glsp generateIndex -h +Usage: glsp generateIndex [options] + +Generate index files in a given source directory. + +Arguments: + rootDir The source directory for index generation. + +Options: + -s, --singleIndex Generate a single index file in the source directory instead of indices in each + sub-directory (default: false) + -f, --forceOverwrite Overwrite existing index files and remove them if there are no entries (default: false) + -m, --match [match patterns...] File patterns to consider during indexing (default: ["**/*.ts","**/*.tsx"]) + -i, --ignore [ignore patterns...] File patterns to ignore during indexing (default: + ["**/*.spec.ts","**/*.spec.tsx","**/*.d.ts"]) + --style Import Style (choices: "commonjs", "esm", default: "commonjs") + --ignoreFile The file that is used to specify patterns that should be ignored during indexing (default: + ".indexignore") + -v, --verbose Generate verbose output during generation (default: false) + -h, --help display help for command +``` + +## releng + +Commands for GLSP release engineering. +These commands are intended for CI usage and direct usage by Eclipse GLSP committers. +They are not general-purpose commands. Subcommands might rely on tools and CLI commands that are only available in +Linux environments. + +```console +$ glsp releng -h +Usage: glsp releng [options] [command] + +Commands for GLSP release engineering (Linux only, intended for CI/Maintainer use). + +Options: + -h, --help display help for command + +Commands: + version [options] [customVersion] Set the version of all packages in a GLSP repository + prepare [options] [customVersion] Prepare a new release for a GLSP component (version bump, changelog, PR + creation ...) + publish [options] Publish all workspace packages of a GLSP repository + help [command] display help for command +``` + +### version + +Command to bump the version of all packages in a GLSP repository. +This bumps the version of all workspace packages (the root `package.json` version is the source of truth). +In addition, external GLSP dependencies are considered and bumped as well; `workspace:` ranges are preserved. +The glsp repository type ("glsp-client", "glsp-server-node" etc.) is auto detected from the given repository path. +If the command is invoked in a non-GLSP repository it will fail. + +```console +$ glsp releng version -h +Usage: glsp releng version [options] [customVersion] + +Set the version of all packages in a GLSP repository + +Arguments: + versionType The version type (choices: "major", "minor", "patch", "custom", "next") + customVersion Custom version number. Will be ignored if the release type is not "custom" + +Options: + -v, --verbose Enable verbose (debug) log output (default: false) + -r, --repoDir Path to the component repository (default: + "") + -h, --help display help for command +``` + +### prepare + +Prepares a new release for a GLSP repository. +This includes bumping the version, updating the changelog, commit & push the changes +and opening a PR for the release. + +The glsp repository type ("glsp-client", "glsp-server-node" etc.) is auto detected from the given repository path. +If the command is invoked in a non-GLSP repository it will fail. + +```console +$ glsp releng prepare -h +Usage: glsp releng prepare [options] [customVersion] + +Prepare a new release for a GLSP component (version bump, changelog, PR creation ...) + +Arguments: + versionType The version type (choices: "major", "minor", "patch", "custom", "next") + customVersion Custom version number. Will be ignored if the release type is not "custom" + +Options: + -v, --verbose Enable verbose (debug) log output (default: false) + -r, --repoDir Path to the component repository (default: + "") + --no-push Do not push changes to remote git repository + -d, --draft Create a draft pull request (only if push is enabled) (default: false) + --no-check Skip initial checks for existing dependency versions + -h, --help display help for command +``` + +### publish + +Publishes all (public) workspace packages of a GLSP repository via `pnpm publish -r` (replaces `lerna publish`). + +- `next`: applies a canary version (`.`, e.g. `2.8.0-next.42`) to all + workspace packages and publishes them under the `next` dist-tag. Requires the full git history + (`fetch-depth: 0` in CI) to derive the commit count. +- `latest`: publishes the current package versions under the `latest` dist-tag. Packages whose version + already exists on the registry are skipped. + +Publishing is delegated to `pnpm publish -r`, so `workspace:` dependency ranges are rewritten to exact +versions; npm provenance/trusted publishing (`NPM_CONFIG_PROVENANCE`) is preserved. + +```console +$ glsp releng publish -h +Usage: glsp releng publish [options] + +Publish all workspace packages of a GLSP repository via `pnpm publish` + +Arguments: + distTag The npm dist-tag to publish under (choices: "next", "latest") + +Options: + -v, --verbose Enable verbose (debug) log output (default: false) + -r, --repoDir Path to the component repository (default: "") + --dry-run Derive versions and run `pnpm publish` in dry-run mode without applying changes (default: false) + --registry Publish to a custom npm registry (e.g. a local verdaccio for testing) + -h, --help display help for command +``` + +## repo + +Multi-repository workspace management for GLSP development. +All repositories are expected to live as siblings in a shared workspace directory (e.g. `~/glsp/glsp-client`, `~/glsp/glsp-server-node`, etc.). +Repositories are auto-discovered by scanning the workspace directory for known GLSP repo names. +The workspace directory is resolved automatically by walking up from the current directory; it can be overridden with `--dir`. +The clone protocol is auto-detected: if the GitHub CLI (`gh`) is installed and authenticated, `gh` is used; otherwise `https`. +This can be overridden per command via `--protocol`. + +```bash +$ glsp repo -h +Usage: glsp repo [options] [command] + +Multi-repository management for GLSP projects + +Options: + -h, --help display help for command + +Commands: + clone [options] [repos...] Clone GLSP repositories + fork [options] Add fork remotes to already-cloned repositories + build [options] Build repositories (dependency-ordered) + link [options] Interlink repositories via pnpm-workspace.yaml link overrides + unlink [options] Remove the pnpm link overrides between repositories + pwd [options] Print resolved paths for all discovered repositories + log [options] Print the last commit for all discovered repositories + workspace Manage VS Code workspace files for GLSP projects + glsp Operations on the glsp repository + glsp-server-node|server-node Operations on the glsp-server-node repository + glsp-client|client Operations on the glsp-client repository + glsp-theia-integration|theia Operations on the glsp-theia-integration repository + glsp-vscode-integration|vscode Operations on the glsp-vscode-integration repository + glsp-eclipse-integration|eclipse Operations on the glsp-eclipse-integration repository + glsp-server|server-java Operations on the glsp-server repository + glsp-playwright|playwright Operations on the glsp-playwright repository + help [command] display help for command +``` + +### clone + +Clones GLSP repositories into the workspace directory. +Repositories can be specified as positional arguments, via `--preset`, or interactively with `--interactive`. + +```console +$ glsp repo clone -h +Usage: glsp repo clone [options] [repos...] + +Clone GLSP repositories + +Arguments: + repos Repositories to clone (can combine with --preset) + +Options: + -d, --dir Target directory for repo clones + -p, --protocol Git clone protocol (default: gh|https) (choices: "ssh", "https", "gh") + -b, --branch Branch or tag to check out after cloning + --fork Clone from a fork and set up dual-remote (origin=fork, upstream=eclipse-glsp) + --override How to handle an existing target directory (choices: "rename", "remove") + --preset Clone repos from a preset (choices: "core", "theia", "vscode", + "eclipse", "playwright", "all") + -i, --interactive Guided setup: choose preset, protocol, and fork interactively (default: false) + --no-fail-fast Continue cloning after a failure + -v, --verbose Verbose output (default: false) + -h, --help display help for command +``` + +### fork + +Adds fork remotes to already-cloned repositories. For each repo, restructures the git remotes so that +`origin` points to your fork and `upstream` points to `eclipse-glsp`. If the fork doesn't exist on GitHub +and the `gh` CLI is available, it will create one. + +```console +$ glsp repo fork -h +Usage: glsp repo fork [options] + +Add fork remotes to already-cloned repositories + +Arguments: + user GitHub username for the fork + +Options: + -d, --dir Target directory where repos are cloned + -p, --protocol Git clone protocol (default: gh|https) (choices: "ssh", "https", "gh") + -r, --repo Fork only these repos + --preset Fork repos from a preset (choices: "core", "theia", "vscode", + "eclipse", "playwright", "all") + -v, --verbose Verbose output (default: false) + -h, --help display help for command +``` + +### build + +Builds repositories in dependency order. Understands the GLSP dependency graph and builds prerequisites first. + +```console +$ glsp repo build -h +Usage: glsp repo build [options] + +Build repositories (dependency-ordered) + +Options: + -d, --dir Target directory where repos are cloned + -r, --repo Build only these repos + --preset Build repos from a preset (choices: "core", "theia", "vscode", + "eclipse", "playwright", "all") + --electron Build Theia electron variant instead of browser (default: false) + --no-java Skip repositories that require Java/Maven + --no-fail-fast Continue building after a failure + -v, --verbose Verbose output (default: false) + -h, --help display help for command +``` + +### link / unlink + +Links (or unlinks) repositories for cross-repo development by injecting `link:` overrides into each +consumer's `pnpm-workspace.yaml` and reinstalling. Repositories are processed in dependency order, and +singleton dependencies (sprotty, sprotty-protocol, vscode-jsonrpc, inversify) are shared from `glsp-client` +to avoid duplicate instances. After linking a repo it is **built** so the `link:` overrides resolve to +compiled `lib/` output rather than empty source directories (pass `--no-build` to skip); only the npm/pnpm +side is built, so the `glsp-eclipse-integration` Maven server is left to a separate build. `unlink` removes +those overrides again and reinstalls. + +For `glsp-eclipse-integration`, whose linkable pnpm workspace lives in a `client/` subdirectory, the +overrides are injected into `client/pnpm-workspace.yaml`. + +```console +$ glsp repo link -h +Usage: glsp repo link [options] + +Interlink repositories via pnpm-workspace.yaml link overrides + +Options: + -d, --dir Target directory where repos are cloned + -r, --repo Link only these repos + --preset Link repos from a preset (choices: "core", "theia", "vscode", + "eclipse", "playwright", "all") + --no-build Skip building the linked repos (links resolve to existing compiled output) + --electron Build the Theia electron variant instead of browser (default: false) + --no-fail-fast Continue after a failure + -v, --verbose Verbose output (default: false) + -h, --help display help for command +``` + +### pwd + +```console +$ glsp repo pwd -h +Usage: glsp repo pwd [options] + +Print resolved paths for all discovered repositories + +Options: + -d, --dir Target directory where repos are cloned + --raw Print repopath per line, no color (default: false) + -v, --verbose Verbose output (default: false) + -h, --help display help for command +``` + +### log + +```console +$ glsp repo log -h +Usage: glsp repo log [options] + +Print the last commit for all discovered repositories + +Options: + -d, --dir Target directory where repos are cloned + -r, --repo Log only these repos + --preset Log repos from a preset (choices: "core", "theia", "vscode", + "eclipse", "playwright", "all") + -v, --verbose Verbose output (default: false) + -h, --help display help for command +``` + +### workspace + +Manage VS Code multi-root workspace files. + +```console +$ glsp repo workspace init -h +Usage: glsp repo workspace init [options] + +Generate a VS Code multi-root workspace file + +Options: + -d, --dir Target directory where repos are cloned + -o, --output Output path for the workspace file + -r, --repo Include only these repos + --preset Include repos from a preset (choices: "core", "theia", "vscode", + "eclipse", "playwright", "all") + -v, --verbose Verbose output (default: false) + -h, --help display help for command +``` + +```console +$ glsp repo workspace open -h +Usage: glsp repo workspace open [options] + +Open the VS Code workspace file + +Options: + -d, --dir Target directory where repos are cloned + -v, --verbose Verbose output (default: false) + -h, --help display help for command +``` + +### Scoped repository commands + +Each repository has a set of scoped subcommands accessible via `glsp repo ` or its short alias. +Short aliases: `client`, `server-node`, `theia`, `vscode`, `eclipse`, `server-java`, `playwright`. + +All repos support `clone`, `switch`, `build`, `pwd`, and `log` subcommands. +Some repos have additional repo-specific commands: + +| Repo | Extra commands | +| ------------------------- | ---------------------- | +| `glsp-client` | `start` | +| `glsp-server-node` | `start` | +| `glsp-server` | `start` | +| `glsp-theia-integration` | `start`, `open` | +| `glsp-vscode-integration` | `vsix-path`, `package` | + +```console +$ glsp repo client -h +Usage: glsp repo glsp-client|client [options] [command] + +Operations on the glsp-client repository + +Commands: + clone [options] Clone the glsp-client repository + switch [options] Switch branch or checkout a PR in glsp-client + build [options] Build the glsp-client repository + pwd [options] Print the resolved path for glsp-client + log [options] Print the last commit for glsp-client + start [options] Start the standalone example for glsp-client + help [command] display help for command +``` + +```console +$ glsp repo vscode -h +Usage: glsp repo glsp-vscode-integration|vscode [options] [command] + +Operations on the glsp-vscode-integration repository + +Commands: + clone [options] Clone the glsp-vscode-integration repository + switch [options] Switch branch or checkout a PR in glsp-vscode-integration + build [options] Build the glsp-vscode-integration repository + pwd [options] Print the resolved path for glsp-vscode-integration + log [options] Print the last commit for glsp-vscode-integration + vsix-path [options] Print the path to the workflow VSIX file + package [options] Package the workflow VS Code extension as a VSIX + help [command] display help for command +``` + +```console +$ glsp repo theia -h +Usage: glsp repo glsp-theia-integration|theia [options] [command] + +Operations on the glsp-theia-integration repository + +Commands: + clone [options] Clone the glsp-theia-integration repository + switch [options] Switch branch or checkout a PR in glsp-theia-integration + build [options] Build the glsp-theia-integration repository + pwd [options] Print the resolved path for glsp-theia-integration + log [options] Print the last commit for glsp-theia-integration + start [options] Start the Theia application for glsp-theia-integration + open [options] Open the Theia application in the browser for glsp-theia-integration + help [command] display help for command +``` + +## More information + +For more information, please visit the [Eclipse GLSP Umbrella repository](https://github.com/eclipse-glsp/glsp) and the [Eclipse GLSP Website](https://www.eclipse.org/glsp/). +If you have questions, please raise them in the [discussions](https://github.com/eclipse-glsp/glsp/discussions) and have a look at our [communication and support options](https://www.eclipse.org/glsp/contact/). diff --git a/dev-packages/cli/bin/glsp.js b/dev-packages/cli/bin/glsp.js new file mode 100755 index 0000000..124439b --- /dev/null +++ b/dev-packages/cli/bin/glsp.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +// Stable launcher committed to the repository so the `glsp` bin symlink can +// always be created at install time. The actual CLI is the esbuild bundle in +// `dist/`, which is a build artifact and may not exist yet on a fresh checkout. +const path = require('path'); +const distEntry = path.join(__dirname, '..', 'dist', 'cli.js'); + +try { + require(distEntry); +} catch (error) { + if (error && error.code === 'MODULE_NOT_FOUND' && error.message.includes(distEntry)) { + console.error("The GLSP CLI has not been built yet. Run 'pnpm build' in the @eclipse-glsp/cli package first."); + process.exit(1); + } + throw error; +} diff --git a/dev-packages/cli/esbuild.js b/dev-packages/cli/esbuild.js new file mode 100644 index 0000000..6dfa455 --- /dev/null +++ b/dev-packages/cli/esbuild.js @@ -0,0 +1,89 @@ +/******************************************************************************** + * Copyright (c) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +const esbuild = require('esbuild'); +const fs = require('fs'); + +const production = process.argv.includes('--production'); +const watch = process.argv.includes('--watch'); + +/** + * @type {import('esbuild').Plugin} + */ +const esbuildProblemMatcherPlugin = { + name: 'esbuild-problem-matcher', + + setup(build) { + build.onStart(() => { + console.log(`${watch ? '[watch]' : ''} build started`); + }); + build.onEnd(result => { + result.errors.forEach(({ text, location }) => { + console.error(`✘ [ERROR] ${text}`); + console.error(` ${location.file}:${location.line}:${location.column}:`); + }); + let resultMessage = ''; + if (watch) { + resultMessage += '\n'; + } + resultMessage += ' build finished'; + if (result.errors.length !== 0) { + resultMessage += `\n✘ ${result.errors.length} error(s)`; + } + if (result.warnings.length !== 0) { + resultMessage += `\n⚠ ${result.warnings.length} warning(s)`; + } + console.log(resultMessage); + }); + } +}; + +async function main() { + const ctx = await esbuild.context({ + entryPoints: ['src/cli.ts'], + bundle: true, + format: 'cjs', + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: 'node', + outfile: 'dist/cli.js', + logLevel: 'silent', + banner: { + js: '#!/usr/bin/env node' + }, + plugins: [ + /* add to the end of plugins array */ + esbuildProblemMatcherPlugin + ] + }); + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + await ctx.dispose(); + // Make the generated CLI file executable + try { + fs.chmodSync('dist/cli.js', 0o755); + } catch (error) { + console.warn('Warning: Could not set executable permissions on dist/cli.js:', error.message); + } + } +} + +main().catch(e => { + console.error(e); + process.exit(1); +}); diff --git a/dev-packages/cli/package.json b/dev-packages/cli/package.json new file mode 100644 index 0000000..46583e0 --- /dev/null +++ b/dev-packages/cli/package.json @@ -0,0 +1,62 @@ +{ + "name": "@eclipse-glsp/cli", + "version": "2.8.0-next", + "description": "CLI Tooling & scripts for GLSP components", + "homepage": "https://www.eclipse.org/glsp/", + "bugs": "https://github.com/eclipse-glsp/glsp/issues", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-glsp/glsp.git" + }, + "license": "(EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)", + "author": { + "name": "Eclipse GLSP" + }, + "contributors": [ + { + "name": "Eclipse GLSP Project", + "email": "glsp-dev@eclipse.org", + "url": "https://projects.eclipse.org/projects/ecd.glsp" + } + ], + "bin": { + "glsp": "bin/glsp.js" + }, + "files": [ + "bin", + "dist" + ], + "scripts": { + "build": "pnpm compile && pnpm bundle", + "build:prod": "pnpm compile && pnpm bundle --production", + "bundle": "pnpm clean:dist && node esbuild.js", + "clean": "rimraf dist tsconfig.tsbuildinfo", + "clean:dist": "rimraf dist", + "compile": "tsc", + "lint": "eslint --ext .ts,.tsx ./src", + "lint:ci": "pnpm lint -o eslint.xml -f checkstyle", + "prepublishOnly": "pnpm build", + "start": "node dist/cli.js", + "test": "vitest run", + "test:all": "pnpm test && pnpm test:e2e", + "test:coverage": "vitest run --coverage", + "test:e2e": "pnpm build && vitest run --config vite.config.e2e.ts", + "test:watch": "vitest", + "watch": "node esbuild.js --watch", + "watch:tsc": "tsc --watch" + }, + "devDependencies": { + "@eclipse-glsp/config-test": "workspace:*", + "@types/semver": "^7.7.1", + "commander": "^14.0.0", + "esbuild": "~0.28.1", + "globby": "^16.2.0", + "minimatch": "^10.2.5", + "open": "^11.0.0", + "semver": "^7.7.2", + "yaml": "^2.8.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/dev-packages/cli/src/cli.spec.ts b/dev-packages/cli/src/cli.spec.ts new file mode 100644 index 0000000..d99a61d --- /dev/null +++ b/dev-packages/cli/src/cli.spec.ts @@ -0,0 +1,528 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { describe, it, expect } from 'vitest'; +import { Command } from 'commander'; +import { CheckHeaderCommand } from './commands/check-header'; +import { GenerateIndex } from './commands/generate-index'; +import { RelengCommand } from './commands/releng/releng'; +import { RepoCommand } from './commands/repo/repo'; +import { UpdateNextCommand } from './commands/update-next'; +import { GLSPRepo } from './util'; +import { createSubrepoCommand } from './commands/repo/subrepos'; + +// ── Helpers ──────────────────────────────────────────────────────────────── + +const optionLongs = (cmd: Command): string[] => cmd.options.map(o => o.long!); +const subcommandNames = (cmd: Command): string[] => cmd.commands.map(c => c.name()); +const findSub = (parent: Command, name: string): Command => { + const sub = parent.commands.find(c => c.name() === name); + expect(sub, `subcommand '${name}' not found on '${parent.name()}'`).toBeDefined(); + return sub!; +}; +const choices = (cmd: Command, long: string): string[] | undefined => { + const opt = cmd.options.find(o => o.long === long); + expect(opt, `option '${long}' not found on '${cmd.name()}'`).toBeDefined(); + return opt!.argChoices; +}; + +// ── Tests ────────────────────────────────────────────────────────────────── + +describe('cli', () => { + // ── Top-level ────────────────────────────────────────────────────── + + describe('top-level commands', () => { + describe('checkHeaders', () => { + it('should accept a argument', () => { + expect(CheckHeaderCommand.registeredArguments).toHaveLength(1); + expect(CheckHeaderCommand.registeredArguments[0].name()).toBe('rootDir'); + expect(CheckHeaderCommand.registeredArguments[0].required).toBe(true); + }); + + it('should have expected options', () => { + expect(optionLongs(CheckHeaderCommand)).toEqual( + expect.arrayContaining([ + '--type', + '--fileExtensions', + '--exclude', + '--no-exclude-defaults', + '--json', + '--autoFix', + '--commit' + ]) + ); + }); + + it('should restrict --type choices', () => { + expect(choices(CheckHeaderCommand, '--type')).toEqual(['full', 'changes', 'lastCommit']); + }); + }); + + describe('updateNext', () => { + it('should have alias "u"', () => { + expect(UpdateNextCommand.alias()).toBe('u'); + }); + + it('should accept an optional [rootDir] argument', () => { + expect(UpdateNextCommand.registeredArguments).toHaveLength(1); + expect(UpdateNextCommand.registeredArguments[0].required).toBe(false); + }); + + it('should have expected options', () => { + expect(optionLongs(UpdateNextCommand)).toEqual(expect.arrayContaining(['--verbose'])); + }); + }); + + describe('generateIndex', () => { + it('should accept a variadic argument', () => { + expect(GenerateIndex.registeredArguments).toHaveLength(1); + expect(GenerateIndex.registeredArguments[0].variadic).toBe(true); + }); + + it('should have expected options', () => { + expect(optionLongs(GenerateIndex)).toEqual( + expect.arrayContaining([ + '--singleIndex', + '--forceOverwrite', + '--match', + '--ignore', + '--style', + '--ignoreFile', + '--verbose' + ]) + ); + }); + + it('should restrict --style choices', () => { + expect(choices(GenerateIndex, '--style')).toEqual(['commonjs', 'esm']); + }); + }); + }); + + // ── Releng ───────────────────────────────────────────────────────── + + describe('releng', () => { + it('should have version and prepare subcommands', () => { + expect(subcommandNames(RelengCommand)).toEqual(expect.arrayContaining(['version', 'prepare'])); + }); + + describe('version', () => { + const cmd = findSub(RelengCommand, 'version'); + + it('should accept argument with choices', () => { + const arg = cmd.registeredArguments[0]; + expect(arg.name()).toBe('versionType'); + expect(arg.required).toBe(true); + expect(arg.argChoices).toEqual(['major', 'minor', 'patch', 'custom', 'next']); + }); + + it('should accept optional [customVersion] argument', () => { + expect(cmd.registeredArguments[1].name()).toBe('customVersion'); + expect(cmd.registeredArguments[1].required).toBe(false); + }); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual(expect.arrayContaining(['--verbose', '--repoDir'])); + }); + }); + + describe('prepare', () => { + const cmd = findSub(RelengCommand, 'prepare'); + + it('should accept argument with choices', () => { + const arg = cmd.registeredArguments[0]; + expect(arg.argChoices).toEqual(['major', 'minor', 'patch', 'custom', 'next']); + }); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual(expect.arrayContaining(['--verbose', '--repoDir', '--no-push', '--draft', '--no-check'])); + }); + }); + }); + + // ── Repo ─────────────────────────────────────────────────────────── + + describe('repo', () => { + it('should register all top-level repo subcommands', () => { + expect(subcommandNames(RepoCommand)).toEqual( + expect.arrayContaining(['clone', 'fork', 'build', 'link', 'unlink', 'pwd', 'log', 'workspace']) + ); + }); + + it('should register a subrepo command for each GLSPRepo', () => { + for (const repoName of GLSPRepo.choices) { + expect(subcommandNames(RepoCommand), `subrepo '${repoName}' should be registered`).toContain(repoName); + } + }); + + it('should have a global --dir option', () => { + expect(optionLongs(RepoCommand)).toContain('--dir'); + }); + + describe('clone', () => { + const cmd = findSub(RepoCommand, 'clone'); + + it('should accept optional [repos...] argument', () => { + expect(cmd.registeredArguments[0].variadic).toBe(true); + expect(cmd.registeredArguments[0].required).toBe(false); + }); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual( + expect.arrayContaining([ + '--dir', + '--protocol', + '--branch', + '--fork', + '--override', + '--preset', + '--interactive', + '--no-fail-fast', + '--verbose' + ]) + ); + }); + + it('should restrict --protocol choices', () => { + expect(choices(cmd, '--protocol')).toEqual(['ssh', 'https', 'gh']); + }); + + it('should restrict --override choices', () => { + expect(choices(cmd, '--override')).toEqual(['rename', 'remove']); + }); + + it('should restrict --preset choices', () => { + expect(choices(cmd, '--preset')).toEqual( + expect.arrayContaining(['core', 'theia', 'vscode', 'eclipse', 'playwright', 'all']) + ); + }); + }); + + describe('fork', () => { + const cmd = findSub(RepoCommand, 'fork'); + + it('should accept required argument', () => { + expect(cmd.registeredArguments[0].name()).toBe('user'); + expect(cmd.registeredArguments[0].required).toBe(true); + }); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual(expect.arrayContaining(['--dir', '--protocol', '--repo', '--preset', '--verbose'])); + }); + + it('should restrict --protocol choices', () => { + expect(choices(cmd, '--protocol')).toEqual(['ssh', 'https', 'gh']); + }); + }); + + describe('build', () => { + const cmd = findSub(RepoCommand, 'build'); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual( + expect.arrayContaining(['--dir', '--repo', '--preset', '--electron', '--no-java', '--no-fail-fast', '--verbose']) + ); + }); + }); + + describe('link', () => { + const cmd = findSub(RepoCommand, 'link'); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual(expect.arrayContaining(['--dir', '--repo', '--preset', '--no-fail-fast', '--verbose'])); + }); + }); + + describe('unlink', () => { + const cmd = findSub(RepoCommand, 'unlink'); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual(expect.arrayContaining(['--dir', '--repo', '--preset', '--no-fail-fast', '--verbose'])); + }); + }); + + describe('pwd', () => { + const cmd = findSub(RepoCommand, 'pwd'); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual(expect.arrayContaining(['--dir', '--raw', '--verbose'])); + }); + }); + + describe('log', () => { + const cmd = findSub(RepoCommand, 'log'); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual(expect.arrayContaining(['--dir', '--repo', '--preset', '--verbose'])); + }); + }); + + describe('workspace', () => { + const workspace = findSub(RepoCommand, 'workspace'); + + it('should have init and open subcommands', () => { + expect(subcommandNames(workspace)).toEqual(expect.arrayContaining(['init', 'open'])); + }); + + describe('init', () => { + const cmd = findSub(workspace, 'init'); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual(expect.arrayContaining(['--dir', '--output', '--repo', '--preset', '--verbose'])); + }); + }); + + describe('open', () => { + const cmd = findSub(workspace, 'open'); + + it('should have expected options', () => { + expect(optionLongs(cmd)).toEqual(expect.arrayContaining(['--dir', '--verbose'])); + }); + }); + }); + + // ── Subrepo commands ─────────────────────────────────────────── + + describe('subrepo commands', () => { + const SHORT_ALIASES: Partial> = { + 'glsp-client': 'client', + 'glsp-server-node': 'server-node', + 'glsp-theia-integration': 'theia', + 'glsp-vscode-integration': 'vscode', + 'glsp-eclipse-integration': 'eclipse', + 'glsp-server': 'server-java', + 'glsp-playwright': 'playwright' + }; + + for (const repoName of GLSPRepo.choices) { + describe(repoName, () => { + const cmd = createSubrepoCommand(repoName); + + it('should have common subcommands', () => { + expect(subcommandNames(cmd)).toEqual(expect.arrayContaining(['clone', 'switch', 'build', 'pwd', 'log', 'run'])); + }); + + if (SHORT_ALIASES[repoName]) { + it(`should have alias "${SHORT_ALIASES[repoName]}"`, () => { + expect(cmd.alias()).toBe(SHORT_ALIASES[repoName]); + }); + } + + describe('scoped clone', () => { + const clone = findSub(cmd, 'clone'); + + it('should have expected options', () => { + expect(optionLongs(clone)).toEqual( + expect.arrayContaining(['--dir', '--protocol', '--branch', '--fork', '--override', '--verbose']) + ); + }); + + it('should restrict --protocol choices', () => { + expect(choices(clone, '--protocol')).toEqual(['ssh', 'https', 'gh']); + }); + + it('should restrict --override choices', () => { + expect(choices(clone, '--override')).toEqual(['rename', 'remove']); + }); + }); + + describe('scoped switch', () => { + const sw = findSub(cmd, 'switch'); + + it('should have expected options', () => { + expect(optionLongs(sw)).toEqual(expect.arrayContaining(['--branch', '--dir', '--pr', '--force', '--verbose'])); + }); + }); + + describe('scoped build', () => { + const build = findSub(cmd, 'build'); + + it('should have expected options', () => { + expect(optionLongs(build)).toEqual(expect.arrayContaining(['--dir', '--verbose'])); + }); + + if (repoName === 'glsp-theia-integration') { + it('should have --electron option', () => { + expect(optionLongs(build)).toContain('--electron'); + }); + } + }); + + describe('scoped pwd', () => { + const pwd = findSub(cmd, 'pwd'); + + it('should have expected options', () => { + expect(optionLongs(pwd)).toEqual(expect.arrayContaining(['--dir', '--verbose'])); + }); + }); + + describe('scoped log', () => { + const log = findSub(cmd, 'log'); + + it('should have expected options', () => { + expect(optionLongs(log)).toEqual(expect.arrayContaining(['--dir', '--verbose'])); + }); + }); + + describe('scoped run', () => { + const run = findSub(cmd, 'run'); + + it('should accept a required + + diff --git a/examples/workflow-server-mcp-demo/public/mcp-service-worker.js b/examples/workflow-server-mcp-demo/public/mcp-service-worker.js new file mode 100644 index 0000000..c82b6d8 --- /dev/null +++ b/examples/workflow-server-mcp-demo/public/mcp-service-worker.js @@ -0,0 +1,111 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// Proxies `fetch('/mcp', ...)` calls from the page to the embedded Web Worker via a MessageChannel +// the page hands over at boot. Lets the page use plain `fetch` (or the MCP SDK's standard +// `StreamableHTTPClientTransport`) without the worker needing to host an HTTP listener. + +const PENDING_TIMEOUT_MS = 60_000; + +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', event => event.waitUntil(self.clients.claim())); + +let workerPort; +let resolveWorkerPort; +let workerPortPromise = new Promise(resolve => { + resolveWorkerPort = resolve; +}); +const pending = new Map(); +let nextId = 1; + +function failPending(id, status, statusText, message) { + const entry = pending.get(id); + if (!entry) return; + pending.delete(id); + clearTimeout(entry.timeout); + entry.resolve({ type: 'mcp-response', id, status, statusText, headers: { 'content-type': 'text/plain' }, body: message }); +} + +function failAllPending(reason) { + for (const id of [...pending.keys()]) { + failPending(id, 503, 'Service Unavailable', reason); + } +} + +self.addEventListener('message', event => { + const data = event.data; + if (!data || data.type !== 'mcp-init-port') { + return; + } + const port = event.ports[0]; + if (!port) { + return; + } + // Port replacement (page reload, worker swap) — orphan any in-flight resolvers cleanly. + if (workerPort && workerPort !== port) { + failAllPending('MCP worker port replaced before response arrived'); + workerPortPromise = new Promise(resolve => { + resolveWorkerPort = resolve; + }); + } + workerPort = port; + workerPort.onmessage = portEvent => { + const reply = portEvent.data; + if (!reply || reply.type !== 'mcp-response') { + return; + } + const entry = pending.get(reply.id); + if (entry) { + pending.delete(reply.id); + clearTimeout(entry.timeout); + entry.resolve(reply); + } + }; + workerPort.start(); + resolveWorkerPort(workerPort); +}); + +self.addEventListener('fetch', event => { + if (new URL(event.request.url).pathname !== '/mcp') { + return; + } + event.respondWith(handleMcp(event.request)); +}); + +async function handleMcp(request) { + if (!workerPort) { + await workerPortPromise; + } + const id = `${Date.now()}-${nextId++}`; + const headers = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + const body = request.method === 'GET' || request.method === 'HEAD' ? null : await request.text(); + const reply = await new Promise(resolve => { + const timeout = setTimeout( + () => failPending(id, 504, 'Gateway Timeout', `MCP worker bridge timed out after ${PENDING_TIMEOUT_MS}ms`), + PENDING_TIMEOUT_MS + ); + pending.set(id, { resolve, timeout }); + workerPort.postMessage({ type: 'mcp-request', id, url: request.url, method: request.method, headers, body }); + }); + return new Response(reply.body, { + status: reply.status, + statusText: reply.statusText, + headers: reply.headers + }); +} diff --git a/examples/workflow-server-mcp-demo/scripts/prepare-dist.mjs b/examples/workflow-server-mcp-demo/scripts/prepare-dist.mjs new file mode 100644 index 0000000..9351140 --- /dev/null +++ b/examples/workflow-server-mcp-demo/scripts/prepare-dist.mjs @@ -0,0 +1,55 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// Populates `dist/` with everything `serve` needs that esbuild doesn't emit: +// - verbatim files from `public/` (index.html, mcp-service-worker.js) +// - the worker bundle from `@eclipse-glsp-examples/workflow-server-bundled-web` +// esbuild writes `index.bundle.js` into the same dir as a separate step. + +import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; + +const require = createRequire(import.meta.url); +const here = new URL('..', import.meta.url).pathname; +const dist = join(here, 'dist'); + +mkdirSync(dist, { recursive: true }); + +// 1. Verbatim assets from ./public/ +const publicDir = join(here, 'public'); +if (existsSync(publicDir)) { + for (const entry of readdirSync(publicDir)) { + const source = join(publicDir, entry); + if (statSync(source).isFile()) { + copyFileSync(source, join(dist, entry)); + } + } +} + +// 2. Worker bundle from the bundled-web dependency +const workerPackage = dirname(require.resolve('@eclipse-glsp-examples/workflow-server-bundled-web/package.json')); +const workerFiles = ['wf-glsp-server-webworker.js', 'wf-glsp-server-webworker.js.map']; +for (const file of workerFiles) { + const source = join(workerPackage, file); + if (!existsSync(source)) { + console.error(`[prepare-dist] Missing ${source} — build workflow-server first.`); + process.exit(1); + } + copyFileSync(source, join(dist, file)); +} + +console.log('[prepare-dist] dist/ populated (public assets + worker bundle)'); diff --git a/examples/workflow-server-mcp-demo/src/index.js b/examples/workflow-server-mcp-demo/src/index.js new file mode 100644 index 0000000..ed07cbd --- /dev/null +++ b/examples/workflow-server-mcp-demo/src/index.js @@ -0,0 +1,905 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { BrowserMessageReader, BrowserMessageWriter, createMessageConnection } from 'vscode-jsonrpc/browser'; + +const GLSP_PROTOCOL_VERSION = '1.0.0'; +const MCP_PROTOCOL_VERSION = '2025-03-26'; +const MCP_URL = '/mcp'; +const DIAGRAM_TYPE = 'workflow-diagram'; +const CLIENT_SESSION_ID = 'smoke-' + Math.random().toString(36).slice(2, 9); + +// Server-pushed action kinds we want forwarded to us via JSON-RPC `process` notifications. +// The framework's `ClientActionForwarder` consults this list; anything not in it triggers a +// "No handler registered" error on the server. Extend as new server-side actions surface. +const CLIENT_ACTION_KINDS = [ + 'status', + 'startProgress', + 'endProgress', + 'requestBounds', + 'setDirtyState', + 'setMarkers', + 'setEditMode', + 'updateModel', + 'setModel', + 'selectAll', + 'selectAction', + 'elementSelected', + 'sourceModelChanged' +]; + +const SVG_NS = 'http://www.w3.org/2000/svg'; +const DIRTY_RENDER_DEBOUNCE_MS = 80; + +const dom = { + log: document.getElementById('log'), + bootStatus: document.getElementById('boot-status'), + systemIndicator: document.getElementById('system-indicator'), + glspSession: document.getElementById('glsp-session'), + mcpSession: document.getElementById('mcp-session'), + btnMcpInit: document.getElementById('btn-mcp-init'), + btnMcpTools: document.getElementById('btn-mcp-tools'), + btnMcpSessionInfo: document.getElementById('btn-mcp-session-info'), + btnMcpElementTypes: document.getElementById('btn-mcp-element-types'), + btnMcpQuery: document.getElementById('btn-mcp-query'), + btnMcpValidate: document.getElementById('btn-mcp-validate'), + btnMcpCreate: document.getElementById('btn-mcp-create'), + btnMcpMove: document.getElementById('btn-mcp-move'), + btnMcpDelete: document.getElementById('btn-mcp-delete'), + btnMcpUndo: document.getElementById('btn-mcp-undo'), + btnMcpRedo: document.getElementById('btn-mcp-redo'), + btnMcpTerminate: document.getElementById('btn-mcp-terminate'), + btnClear: document.getElementById('btn-clear'), + lastResultTitle: document.getElementById('last-result-title'), + lastResult: document.getElementById('last-result'), + diagramDirty: document.getElementById('diagram-dirty'), + diagram: document.getElementById('diagram-canvas'), + infoBar: document.getElementById('info-bar'), + statusLine: document.getElementById('status-line'), + progressRow: document.getElementById('progress-row'), + progressTitle: document.getElementById('progress-title'), + progressBar: document.getElementById('progress-bar') +}; + +function setSystemState(state, label) { + if (!dom.systemIndicator) return; + dom.systemIndicator.dataset.state = state; + dom.systemIndicator.textContent = label; +} + +// ---------- log + helpers ---------- + +function logEntry(kind, summary, payload) { + const entry = document.createElement('details'); + if (kind === 'error') entry.classList.add('error'); + entry.open = true; + const sum = document.createElement('summary'); + sum.textContent = `[${new Date().toLocaleTimeString()}] ${summary}`; + entry.appendChild(sum); + const pre = document.createElement('pre'); + pre.textContent = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2); + entry.appendChild(pre); + dom.log.prepend(entry); +} + +function headersToObject(headers) { + const result = {}; + headers.forEach((value, key) => { + result[key] = value; + }); + return result; +} + +// ---------- GLSP client ---------- + +/** + * Thin wrapper around `vscode-jsonrpc/browser` that knows the handful of GLSP requests + * this smoke needs: `initialize`, `initializeClientSession`, plus `process` notifications + * for action message round-trips (`requestModel` out, server-pushed actions in). + */ +class GlspClient { + /** @param {Worker} worker */ + constructor(worker) { + const reader = new BrowserMessageReader(worker); + const writer = new BrowserMessageWriter(worker); + this.connection = createMessageConnection(reader, writer); + this.connection.onError(err => logEntry('error', 'GLSP JSON-RPC error', String(err))); + this._actionHandler = null; + this.connection.onNotification('process', message => { + if (this._actionHandler && message && message.action) { + this._actionHandler(message.action); + } + }); + this.connection.listen(); + } + + /** @param {(action: any) => void} handler */ + onAction(handler) { + this._actionHandler = handler; + } + + async initialize(applicationId, mcpServerOptions) { + logEntry('request', '→ GLSP initialize', { mcpServer: mcpServerOptions }); + const result = await this.connection.sendRequest('initialize', { + applicationId, + protocolVersion: GLSP_PROTOCOL_VERSION, + mcpServer: mcpServerOptions + }); + logEntry('response', '← GLSP initialize', result); + return result; + } + + async initializeClientSession(clientSessionId, diagramType, clientActionKinds) { + const params = { diagramType, clientSessionId, clientActionKinds, args: {} }; + logEntry('request', '→ GLSP initializeClientSession', params); + await this.connection.sendRequest('initializeClientSession', params); + logEntry('response', '← GLSP initializeClientSession', { ok: true }); + } + + /** Send an action to the server via `process` notification. */ + dispatchAction(clientId, action) { + this.connection.sendNotification('process', { clientId, action }); + } +} + +// ---------- bounds reply (RequestBoundsAction → ComputedBoundsAction) ---------- + +// The workflow MCP serializer drops task elements whose label child has no Dimension. A +// real Sprotty-based client computes those via DOM measurement and replies; here we +// estimate per element type so new nodes survive the serializer's filter. +function estimateBounds(element) { + const type = typeof element.type === 'string' ? element.type : ''; + if (type === 'label:heading' || type.startsWith('label')) { + const textLen = typeof element.text === 'string' ? element.text.length : 5; + return { width: Math.max(20, textLen * 7), height: 16 }; + } + if (type.startsWith('task')) return { width: 80, height: 30 }; + if (type === 'icon') return { width: 25, height: 20 }; + if (type.startsWith('activityNode')) return { width: 32, height: 32 }; + return { width: 60, height: 30 }; +} + +function collectMissingBounds(root) { + const bounds = []; + const stack = [root]; + while (stack.length) { + const element = stack.pop(); + if (!element) continue; + const size = element.size; + const hasSize = size && Number.isFinite(size.width) && Number.isFinite(size.height) && size.width > 0 && size.height > 0; + if (typeof element.id === 'string' && !hasSize) { + bounds.push({ elementId: element.id, newSize: estimateBounds(element) }); + } + if (Array.isArray(element.children)) { + for (const child of element.children) stack.push(child); + } + } + return bounds; +} + +function buildComputedBoundsAction(requestAction) { + // `ComputedBoundsActionHandler` discards the reply unless `revision` matches the + // current `modelState.root.revision`. Mirror the request's newRoot revision. + return { + kind: 'computedBounds', + bounds: collectMissingBounds(requestAction.newRoot), + revision: requestAction.newRoot?.revision, + responseId: requestAction.requestId + }; +} + +// ---------- MCP client ---------- + +let rpcId = 1; +let mcpSessionId = ''; + +function mcpHeaders() { + return mcpSessionId ? { 'mcp-session-id': mcpSessionId, 'mcp-protocol-version': MCP_PROTOCOL_VERSION } : {}; +} + +// Parse a Streamable HTTP MCP response body. The transport returns either plain JSON or +// SSE-framed (`event:` / `data:` lines); we want the JSON payload either way. +async function readResponseBody(response) { + const text = await response.text(); + if (!text) return ''; + const trimmed = text.trim(); + if (trimmed.startsWith('event:') || trimmed.startsWith('data:')) { + for (const line of text.split('\n')) { + const stripped = line.trim(); + if (stripped.startsWith('data:')) { + try { + return JSON.parse(stripped.slice(5).trim()); + } catch { + /* fall through to raw text */ + } + } + } + } + try { + return JSON.parse(text); + } catch { + return text; + } +} + +async function mcpFetch(extraHeaders, jsonBody, summary, options = {}) { + const headers = { + accept: 'application/json, text/event-stream', + 'content-type': 'application/json', + ...extraHeaders + }; + const method = options.method ?? 'POST'; + const fetchInit = { method, headers }; + if (jsonBody !== undefined) { + fetchInit.body = JSON.stringify(jsonBody); + logEntry('request', `→ ${method} ${MCP_URL}`, { headers, body: jsonBody }); + } else { + logEntry('request', `→ ${method} ${MCP_URL}`, { headers }); + } + try { + const response = await fetch(MCP_URL, fetchInit); + const responseHeaders = headersToObject(response.headers); + const body = await readResponseBody(response); + logEntry(response.ok ? 'response' : 'error', `← ${response.status} ${response.statusText} ${summary}`, { + headers: responseHeaders, + body + }); + // `internal` calls are infrastructure round-trips (e.g. the auto `diagram-model` fetch after + // each mutation) — they shouldn't clobber the user-visible last-result panel. + if (!options.internal) { + renderLastResult(summary, body); + } + return { response, headers: responseHeaders, body }; + } catch (err) { + logEntry('error', `← ${summary} failed`, String(err)); + throw err; + } +} + +function mcpToolCall(name, args, summary, options) { + return mcpFetch( + mcpHeaders(), + { jsonrpc: '2.0', id: rpcId++, method: 'tools/call', params: { name, arguments: args } }, + summary, + options + ); +} + +// ---------- last-result panel ---------- + +// Pretty-print the most recent MCP response. Recognises the common shapes the demo's tools +// emit (arrays of {name, description, …}, plain primitive objects, text-only `content`) and +// falls back to indented JSON when it can't summarise meaningfully. +function renderLastResult(summary, body) { + if (!dom.lastResult || !dom.lastResultTitle) return; + dom.lastResultTitle.textContent = summary; + dom.lastResult.replaceChildren(); + const payload = pickRenderPayload(body); + dom.lastResult.appendChild(renderPayload(payload)); +} + +// Strip the JSON-RPC envelope down to the bit a human cares about. +function pickRenderPayload(body) { + if (!body || typeof body !== 'object') return body; + if (body.error) return { error: body.error }; + const result = body.result; + if (!result || typeof result !== 'object') return result ?? body; + // `tools/list` / `resources/list` / `prompts/list` returns { tools | resources | prompts: [...] }. + if (Array.isArray(result.tools)) return result.tools; + if (Array.isArray(result.resources)) return result.resources; + if (Array.isArray(result.prompts)) return result.prompts; + // `tools/call` returns structuredContent (preferred) or content[]. + if (result.structuredContent && typeof result.structuredContent === 'object') return result.structuredContent; + if (Array.isArray(result.content)) return result.content; + return result; +} + +function renderPayload(payload) { + if (Array.isArray(payload) && payload.length > 0 && payload.every(item => item && typeof item === 'object' && !Array.isArray(item))) { + return renderObjectArrayTable(payload); + } + if (payload && typeof payload === 'object' && !Array.isArray(payload)) { + // Plain `content` entries from tools/call — `[{ type: 'text', text: '…' }]` flattened to text. + if (typeof payload.type === 'string' && typeof payload.text === 'string') { + const pre = document.createElement('pre'); + pre.textContent = payload.text; + return pre; + } + if (Object.values(payload).every(v => typeof v !== 'object' || v === null)) { + return renderKeyValueTable(payload); + } + } + const pre = document.createElement('pre'); + pre.textContent = JSON.stringify(payload, null, 2); + return pre; +} + +function renderObjectArrayTable(items) { + // Pick a small set of "interesting" columns. `name`/`title` and `description` cover the + // tools/list, resources/list, and query-elements shapes; fall back to whatever keys exist. + const preferred = ['name', 'title', 'description', 'id', 'type', 'elementTypeId', 'label']; + const present = preferred.filter(key => items.some(item => key in item)); + const keys = present.length > 0 ? present : Object.keys(items[0]).slice(0, 4); + const table = document.createElement('table'); + const thead = document.createElement('thead'); + const headRow = document.createElement('tr'); + for (const key of keys) { + const th = document.createElement('th'); + th.textContent = key; + headRow.appendChild(th); + } + thead.appendChild(headRow); + table.appendChild(thead); + const tbody = document.createElement('tbody'); + for (const item of items) { + const row = document.createElement('tr'); + for (const key of keys) { + const td = document.createElement('td'); + td.className = key === keys[0] ? 'key' : 'value'; + const value = item[key]; + td.textContent = value === undefined ? '' : typeof value === 'string' ? value : JSON.stringify(value); + row.appendChild(td); + } + tbody.appendChild(row); + } + table.appendChild(tbody); + return table; +} + +function renderKeyValueTable(payload) { + const table = document.createElement('table'); + const tbody = document.createElement('tbody'); + for (const [key, value] of Object.entries(payload)) { + const row = document.createElement('tr'); + const keyCell = document.createElement('td'); + keyCell.className = 'key'; + keyCell.textContent = key; + const valueCell = document.createElement('td'); + valueCell.className = 'value'; + valueCell.textContent = value === null ? 'null' : String(value); + row.append(keyCell, valueCell); + tbody.appendChild(row); + } + table.appendChild(tbody); + return table; +} + +// ---------- info bar (server-pushed actions) ---------- + +function showInfoBar() { + dom.infoBar.classList.add('visible'); + document.getElementById('server-messages-section')?.classList.remove('section-hidden'); +} + +function renderStatus(action) { + const severity = (action.severity ?? 'info').toLowerCase(); + const message = (action.message ?? '').trim(); + // `{severity: 'NONE', message: ''}` is a "clear status" beat between meaningful + // updates; render that as a quiet idle line rather than verbatim text. + if (severity === 'none' && !message) { + dom.statusLine.textContent = 'All quiet.'; + } else if (!message) { + dom.statusLine.textContent = severity[0].toUpperCase() + severity.slice(1); + } else { + dom.statusLine.textContent = message; + } + showInfoBar(); +} + +function startProgress(action) { + dom.progressTitle.textContent = action.title ?? 'In progress…'; + dom.progressBar.removeAttribute('value'); + dom.progressRow.style.display = 'flex'; + showInfoBar(); +} + +function endProgress() { + dom.progressRow.style.display = 'none'; +} + +// ---------- diagram rendering ---------- + +function nodeCssClass(type) { + if (type.startsWith('task:automated')) return 'diagram-node task-automated'; + if (type.startsWith('task:manual')) return 'diagram-node task-manual'; + if (type.startsWith('activityNode')) return 'diagram-node activity-node'; + return 'diagram-node'; +} + +function elementLabel(element) { + if (typeof element.label === 'string' && element.label.trim()) return element.label; + if (typeof element.name === 'string' && element.name.trim()) return element.name; + if (typeof element.type === 'string' && element.type.includes(':')) { + return element.type.slice(element.type.lastIndexOf(':') + 1); + } + return element.id; +} + +// Painted geometry for a node, derived from its bounding rect. Diamonds shrink to +// 75% of the rect so they don't read as overscaled. Synchronization bars (fork/join) +// align to the long axis of the bounding rect — vertical when the rect is tall +// (workflow's canonical 10×50), horizontal when wide. +const DIAMOND_SCALE = 0.75; +const BAR_THICKNESS = 4; +function nodeShape(node) { + const type = node.type ?? ''; + const x = node.position.x; + const y = node.position.y; + const w = node.size.width; + const h = node.size.height; + const cx = x + w / 2; + const cy = y + h / 2; + if (type === 'activityNode:decision' || type === 'activityNode:merge') { + return { kind: 'diamond', cx, cy, halfW: (w * DIAMOND_SCALE) / 2, halfH: (h * DIAMOND_SCALE) / 2 }; + } + if (type === 'activityNode:fork' || type === 'activityNode:join') { + if (h >= w) { + // Vertical bar — thin column along the long (vertical) axis. + return { kind: 'rect', x: cx - BAR_THICKNESS / 2, y, width: BAR_THICKNESS, height: h }; + } + return { kind: 'rect', x, y: cy - BAR_THICKNESS / 2, width: w, height: BAR_THICKNESS }; + } + return { kind: 'rect', x, y, width: w, height: h }; +} + +// Clip the line from the shape's center toward (fromX,fromY) to the shape's boundary, +// so edges meet the painted shape instead of disappearing into it or stopping short. +function clipLineToShape(shape, fromX, fromY) { + if (shape.kind === 'diamond') { + const dx = fromX - shape.cx; + const dy = fromY - shape.cy; + if (dx === 0 && dy === 0) return { x: shape.cx, y: shape.cy }; + // Diamond |X|/halfW + |Y|/halfH = 1 → r = 1 / (|dx|/halfW + |dy|/halfH). + const r = 1 / (Math.abs(dx) / shape.halfW + Math.abs(dy) / shape.halfH); + return { x: shape.cx + dx * r, y: shape.cy + dy * r }; + } + const cx = shape.x + shape.width / 2; + const cy = shape.y + shape.height / 2; + const dx = fromX - cx; + const dy = fromY - cy; + if (dx === 0 && dy === 0) return { x: cx, y: cy }; + const scale = Math.min(shape.width / 2 / Math.abs(dx || Infinity), shape.height / 2 / Math.abs(dy || Infinity)); + return { x: cx + dx * scale, y: cy + dy * scale }; +} + +function svg(tag, attrs = {}) { + const node = document.createElementNS(SVG_NS, tag); + for (const [key, value] of Object.entries(attrs)) { + node.setAttribute(key, String(value)); + } + return node; +} + +function partitionElements(elements) { + const nodes = []; + const edges = []; + const nodeIndex = new Map(); + for (const element of elements) { + const type = element.type ?? ''; + if (typeof element.sourceId === 'string' && typeof element.targetId === 'string') { + edges.push(element); + } else if (element.position && element.size && (type.startsWith('task') || type.startsWith('activityNode'))) { + nodes.push(element); + nodeIndex.set(element.id, element); + } + } + return { nodes, edges, nodeIndex }; +} + +function computeViewBox(nodes) { + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; + for (const node of nodes) { + const { x, y } = node.position; + const { width, height } = node.size; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x + width > maxX) maxX = x + width; + if (y + height > maxY) maxY = y + height; + } + const padding = 20; + const width = Math.max(maxX - minX + padding * 2, 200); + const height = Math.max(maxY - minY + padding * 2, 100); + return { x: minX - padding, y: minY - padding, width, height }; +} + +function renderDiagram(elements) { + dom.diagram.replaceChildren(); + const { nodes, edges, nodeIndex } = partitionElements(elements); + if (nodes.length === 0) { + dom.diagram.classList.remove('visible'); + logEntry('error', 'No renderable nodes in diagram-model output', { elements }); + return; + } + + const viewBox = computeViewBox(nodes); + dom.diagram.setAttribute('viewBox', `${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`); + const displayWidth = Math.min(viewBox.width, 960); + dom.diagram.setAttribute('width', String(displayWidth)); + dom.diagram.setAttribute('height', String(viewBox.height * (displayWidth / viewBox.width))); + + const defs = svg('defs'); + const marker = svg('marker', { id: 'arrow', markerWidth: 10, markerHeight: 10, refX: 8, refY: 3, orient: 'auto' }); + marker.appendChild(svg('path', { d: 'M0,0 L0,6 L8,3 z', class: 'diagram-edge-arrow' })); + defs.appendChild(marker); + dom.diagram.appendChild(defs); + + // Precompute painted geometry once per node so edge clipping and shape painting + // agree on where the visible boundary actually is. + const shapes = new Map(); + for (const node of nodes) { + shapes.set(node.id, nodeShape(node)); + } + + // Edges first so nodes paint on top. Both endpoints clip to the painted shape so + // the arrow head lands on the shape's edge, not behind it or short of it. + for (const edge of edges) { + const sourceShape = shapes.get(edge.sourceId); + const targetShape = shapes.get(edge.targetId); + if (!sourceShape || !targetShape) continue; + const sourceCx = sourceShape.kind === 'diamond' ? sourceShape.cx : sourceShape.x + sourceShape.width / 2; + const sourceCy = sourceShape.kind === 'diamond' ? sourceShape.cy : sourceShape.y + sourceShape.height / 2; + const targetCx = targetShape.kind === 'diamond' ? targetShape.cx : targetShape.x + targetShape.width / 2; + const targetCy = targetShape.kind === 'diamond' ? targetShape.cy : targetShape.y + targetShape.height / 2; + const start = clipLineToShape(sourceShape, targetCx, targetCy); + const end = clipLineToShape(targetShape, sourceCx, sourceCy); + dom.diagram.appendChild( + svg('line', { + x1: start.x, + y1: start.y, + x2: end.x, + y2: end.y, + class: 'diagram-edge', + 'marker-end': 'url(#arrow)' + }) + ); + } + + for (const node of nodes) { + const type = node.type ?? ''; + const cssClass = nodeCssClass(type); + const shape = shapes.get(node.id); + if (shape.kind === 'diamond') { + const points = `${shape.cx},${shape.cy - shape.halfH} ${shape.cx + shape.halfW},${shape.cy} ${shape.cx},${shape.cy + shape.halfH} ${shape.cx - shape.halfW},${shape.cy}`; + dom.diagram.appendChild(svg('polygon', { points, class: cssClass })); + } else if (type.startsWith('activityNode')) { + // Bar — flat filled rect, no inset label. + dom.diagram.appendChild( + svg('rect', { x: shape.x, y: shape.y, width: shape.width, height: shape.height, rx: 1.5, class: cssClass }) + ); + } else { + dom.diagram.appendChild( + svg('rect', { x: shape.x, y: shape.y, width: shape.width, height: shape.height, rx: 4, class: cssClass }) + ); + const text = svg('text', { + x: shape.x + shape.width / 2, + y: shape.y + shape.height / 2, + class: 'diagram-label' + }); + text.textContent = elementLabel(node); + dom.diagram.appendChild(text); + } + } + + dom.diagram.classList.add('visible'); +} + +async function fetchAndRenderDiagram() { + const result = await mcpToolCall('diagram-model', { sessionId: CLIENT_SESSION_ID }, '(diagram-model)', { internal: true }); + const elements = result.body?.result?.structuredContent?.elements; + if (Array.isArray(elements)) { + renderDiagram(elements); + } else { + logEntry('error', 'diagram-model response missing structuredContent.elements', result.body); + } +} + +// Re-render on every `updateModel` push (debounced). `updateModel` is the canonical +// GLSP signal carrying the new model root — Sprotty-based clients swap the root on +// this action; we re-fetch `diagram-model` for the same effect. +let updateRenderTimer = 0; +function scheduleModelRender() { + if (!mcpSessionId || updateRenderTimer) return; + updateRenderTimer = setTimeout(() => { + updateRenderTimer = 0; + fetchAndRenderDiagram().catch(err => logEntry('error', 'auto re-render on updateModel failed', String(err))); + }, DIRTY_RENDER_DEBOUNCE_MS); +} + +// ---------- boot ---------- + +let glsp; + +async function bootGlsp(worker) { + glsp = new GlspClient(worker); + glsp.onAction(action => { + switch (action.kind) { + case 'status': + renderStatus(action); + break; + case 'updateModel': + scheduleModelRender(); + break; + case 'setDirtyState': + if (dom.diagramDirty) { + dom.diagramDirty.hidden = !action.isDirty; + } + break; + case 'startProgress': + startProgress(action); + break; + case 'endProgress': + endProgress(); + break; + case 'requestBounds': + glsp.dispatchAction(CLIENT_SESSION_ID, buildComputedBoundsAction(action)); + break; + default: + logEntry('response', `← server-pushed action ${action.kind}`, action); + } + }); + + await glsp.initialize('workflow-server-bundled-web-smoke', { name: 'workflow-glsp', route: MCP_URL }); + + dom.glspSession.value = CLIENT_SESSION_ID; + await glsp.initializeClientSession(CLIENT_SESSION_ID, DIAGRAM_TYPE, CLIENT_ACTION_KINDS); + + // Trigger model load. `WorkflowMockModelStorage` ignores the URI and always serves + // its bundled example1.json, so any non-empty `sourceUri` works. + logEntry('request', '→ GLSP process(requestModel)', { sourceUri: 'example1.json' }); + glsp.dispatchAction(CLIENT_SESSION_ID, { + kind: 'requestModel', + options: { sourceUri: 'example1.json', diagramType: DIAGRAM_TYPE } + }); +} + +async function bootSw(worker) { + if (!('serviceWorker' in navigator)) { + throw new Error('Service Workers not supported in this browser'); + } + await navigator.serviceWorker.register('./mcp-service-worker.js'); + await navigator.serviceWorker.ready; + if (!navigator.serviceWorker.controller) { + // First registration: SW activated but the page isn't controlled yet. With + // `skipWaiting`/`clients.claim` this usually fires within a tick. + await new Promise(resolve => { + navigator.serviceWorker.addEventListener('controllerchange', resolve, { once: true }); + }); + } + // One MessageChannel; the page holds neither end. Port 1 goes to the Service Worker + // (incoming MCP fetches), port 2 to the Web Worker (MCP request handler). + const channel = new MessageChannel(); + navigator.serviceWorker.controller.postMessage({ type: 'mcp-init-port' }, [channel.port1]); + worker.postMessage({ type: 'mcp-init-port' }, [channel.port2]); +} + +async function boot() { + // Cache-bust the worker bundle URL per page load. Browsers HTTP-cache `new Worker(url)` + // fetches; without this, rebuilding the bundle takes effect only after a Service Worker + // unregister or cache flush. + const worker = new Worker(`./wf-glsp-server-webworker.js?v=${Date.now()}`); + worker.addEventListener('error', event => logEntry('error', 'Worker error', event.message ?? String(event))); + + try { + await Promise.all([bootGlsp(worker), bootSw(worker)]); + dom.bootStatus.textContent = `Connected. Workflow session ${CLIENT_SESSION_ID} is open.`; + dom.bootStatus.classList.add('ready'); + setSystemState('ready', 'Ready'); + dom.btnMcpInit.disabled = false; + } catch (err) { + dom.bootStatus.textContent = `Boot failed — ${err.message ?? err}`; + dom.bootStatus.classList.add('failed'); + setSystemState('failed', 'Failed'); + logEntry('error', 'Boot failed', String(err)); + } +} + +// ---------- button wiring ---------- + +dom.btnMcpInit.addEventListener('click', async () => { + dom.btnMcpInit.disabled = true; + const result = await mcpFetch( + {}, + { + jsonrpc: '2.0', + id: rpcId++, + method: 'initialize', + params: { + protocolVersion: MCP_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'glsp-mcp-smoke', version: '0.0.1' } + } + }, + '(initialize)' + ); + mcpSessionId = result.headers['mcp-session-id'] ?? ''; + if (!mcpSessionId) { + logEntry('error', 'No mcp-session-id in response headers', result.headers); + dom.btnMcpInit.disabled = false; + return; + } + dom.mcpSession.value = mcpSessionId; + // Demote the primary treatment once init has succeeded — every action is now equal. + dom.btnMcpInit.classList.remove('primary'); + dom.btnMcpTools.disabled = false; + dom.btnMcpSessionInfo.disabled = false; + dom.btnMcpElementTypes.disabled = false; + dom.btnMcpQuery.disabled = false; + dom.btnMcpValidate.disabled = false; + dom.btnMcpCreate.disabled = false; + dom.btnMcpTerminate.disabled = false; + // Move/Delete activate after the first create; Undo/Redo follow the dispatched-commands stack. + await mcpFetch(mcpHeaders(), { jsonrpc: '2.0', method: 'notifications/initialized' }, '(initialized)'); + await fetchAndRenderDiagram(); + dom.btnMcpInit.disabled = false; +}); + +dom.btnMcpTools.addEventListener('click', () => { + mcpFetch(mcpHeaders(), { jsonrpc: '2.0', id: rpcId++, method: 'tools/list' }, '(tools/list)'); +}); + +dom.btnMcpSessionInfo.addEventListener('click', () => { + mcpToolCall('session-info', {}, '(session-info)'); +}); + +dom.btnMcpElementTypes.addEventListener('click', () => { + mcpToolCall('element-types', { sessionId: CLIENT_SESSION_ID }, '(element-types)'); +}); + +dom.btnMcpQuery.addEventListener('click', () => { + mcpToolCall('query-elements', { sessionId: CLIENT_SESSION_ID }, '(query-elements)'); +}); + +dom.btnMcpValidate.addEventListener('click', () => { + mcpToolCall('validate-diagram', { sessionId: CLIENT_SESSION_ID }, '(validate-diagram)'); +}); + +dom.btnMcpUndo.addEventListener('click', async () => { + const count = undoStack[undoStack.length - 1]; + if (!count) return; + dom.btnMcpUndo.disabled = true; + await mcpToolCall('undo', { sessionId: CLIENT_SESSION_ID, commandsToUndo: count }, `(undo · ${count})`); + undoStack.pop(); + redoStack.push(count); + updateUndoRedoButtons(); +}); +dom.btnMcpRedo.addEventListener('click', async () => { + const count = redoStack[redoStack.length - 1]; + if (!count) return; + dom.btnMcpRedo.disabled = true; + await mcpToolCall('redo', { sessionId: CLIENT_SESSION_ID, commandsToRedo: count }, `(redo · ${count})`); + redoStack.pop(); + undoStack.push(count); + updateUndoRedoButtons(); +}); + +// Each click drops a new manual task with an incrementing position so repeated clicks +// don't overlap. The `updateModel` push from the server auto-triggers a re-render. +let createOffset = 0; +const recentTaskIds = []; +// Each mutating tool reports `dispatchedCommands` — the number of underlying GLSP commands the +// call produced (e.g. create-with-label = 2 commands). Track per-call counts so Undo / Redo can +// roll back / replay a full user action instead of just its last sub-command. +const undoStack = []; +const redoStack = []; +function updateUndoRedoButtons() { + dom.btnMcpUndo.disabled = undoStack.length === 0; + dom.btnMcpRedo.disabled = redoStack.length === 0; +} +function trackDispatched(toolResult) { + const count = toolResult?.body?.result?.structuredContent?.dispatchedCommands; + if (typeof count === 'number' && count > 0) { + undoStack.push(count); + redoStack.length = 0; + updateUndoRedoButtons(); + } +} +function rememberCreatedTaskIds(toolResult) { + const created = toolResult?.body?.result?.structuredContent?.createdNodes; + if (!Array.isArray(created)) return; + for (const node of created) { + // ElementIdentitySchema fields: `id` is the (aliased) element id. + if (node && typeof node.id === 'string') { + recentTaskIds.push(node.id); + } + } + if (recentTaskIds.length > 0) { + dom.btnMcpMove.disabled = false; + dom.btnMcpDelete.disabled = false; + } +} +dom.btnMcpCreate.addEventListener('click', async () => { + dom.btnMcpCreate.disabled = true; + createOffset += 1; + const result = await mcpToolCall( + 'create-nodes', + { + sessionId: CLIENT_SESSION_ID, + nodes: [ + { + elementTypeId: 'task:manual', + position: { x: 40, y: 30 + createOffset * 50 }, + text: `Task ${createOffset}` + } + ] + }, + '(create-nodes)' + ); + rememberCreatedTaskIds(result); + trackDispatched(result); + dom.btnMcpCreate.disabled = false; +}); + +// Move/delete target the most recently created task. Both rely on `updateModel` +// for the diagram re-render. +dom.btnMcpMove.addEventListener('click', async () => { + const elementId = recentTaskIds[recentTaskIds.length - 1]; + if (!elementId) return; + dom.btnMcpMove.disabled = true; + const result = await mcpToolCall( + 'modify-nodes', + { + sessionId: CLIENT_SESSION_ID, + nodes: [{ elementId, position: { x: 220, y: 30 + createOffset * 50 } }] + }, + '(modify-nodes · move)' + ); + trackDispatched(result); + dom.btnMcpMove.disabled = false; +}); +dom.btnMcpDelete.addEventListener('click', async () => { + const elementId = recentTaskIds.pop(); + if (!elementId) return; + dom.btnMcpDelete.disabled = true; + const result = await mcpToolCall('delete-elements', { sessionId: CLIENT_SESSION_ID, elementIds: [elementId] }, '(delete-elements)'); + trackDispatched(result); + if (recentTaskIds.length === 0) { + dom.btnMcpMove.disabled = true; + dom.btnMcpDelete.disabled = true; + } else { + dom.btnMcpDelete.disabled = false; + } +}); + +// Terminate the current MCP session via the spec's `DELETE /mcp` op. After this the server +// drops the per-session state; subsequent tool calls fail with 404 until the user re-initialises. +dom.btnMcpTerminate.addEventListener('click', async () => { + if (!mcpSessionId) return; + dom.btnMcpTerminate.disabled = true; + await mcpFetch(mcpHeaders(), undefined, '(terminate)', { method: 'DELETE' }); + mcpSessionId = ''; + dom.mcpSession.value = ''; + // Tool buttons can no longer succeed against this session; the user must re-Initialize. + dom.btnMcpTools.disabled = true; + dom.btnMcpSessionInfo.disabled = true; + dom.btnMcpElementTypes.disabled = true; + dom.btnMcpQuery.disabled = true; + dom.btnMcpValidate.disabled = true; + dom.btnMcpCreate.disabled = true; + dom.btnMcpMove.disabled = true; + dom.btnMcpDelete.disabled = true; + dom.btnMcpUndo.disabled = true; + dom.btnMcpRedo.disabled = true; + dom.btnMcpInit.disabled = false; + dom.btnMcpInit.classList.add('primary'); +}); + +dom.btnClear.addEventListener('click', () => { + dom.log.innerHTML = ''; +}); + +boot(); diff --git a/examples/workflow-server/LICENSE b/examples/workflow-server/LICENSE new file mode 100644 index 0000000..e23ece2 --- /dev/null +++ b/examples/workflow-server/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. \ No newline at end of file diff --git a/examples/workflow-server/README.md b/examples/workflow-server/README.md new file mode 100644 index 0000000..a9c6488 --- /dev/null +++ b/examples/workflow-server/README.md @@ -0,0 +1,18 @@ +# Eclipse GLSP - Workflow Examples + +Provides the typescript implementation of the Workflow example server. This example server is used as dev example in GLSP projects. + +## Workflow Diagram Example + +The workflow diagram is a consistent example provided by all GLSP components. +The example implements a simple flow chart diagram editor with different types of nodes and edges (see screenshot below). +The example can be used to try out different GLSP features, as well as several available integrations with IDE platforms (Theia, VSCode, Eclipse, Standalone). +As the example is fully open source, you can also use it as a blueprint for a custom implementation of a GLSP diagram editor. +See [our project website](https://www.eclipse.org/glsp/documentation/#workflowoverview) for an overview of the workflow example and all components implementing it. + + + +## More information + +For more information, please visit the [Eclipse GLSP Umbrella repository](https://github.com/eclipse-glsp/glsp) and the [Eclipse GLSP Website](https://www.eclipse.org/glsp/). +If you have questions, please raise them in the [discussions](https://github.com/eclipse-glsp/glsp/discussions) and have a look at our [communication and support options](https://www.eclipse.org/glsp/contact/). diff --git a/examples/workflow-server/browser.d.ts b/examples/workflow-server/browser.d.ts new file mode 100644 index 0000000..64126dd --- /dev/null +++ b/examples/workflow-server/browser.d.ts @@ -0,0 +1,16 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +export * from './lib/browser/index'; diff --git a/examples/workflow-server/browser.js b/examples/workflow-server/browser.js new file mode 100644 index 0000000..5f3c029 --- /dev/null +++ b/examples/workflow-server/browser.js @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (c) 2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +'use strict'; + +module.exports = require('./lib/browser/index'); diff --git a/examples/workflow-server/esbuild.js b/examples/workflow-server/esbuild.js new file mode 100644 index 0000000..2c63856 --- /dev/null +++ b/examples/workflow-server/esbuild.js @@ -0,0 +1,140 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +// @ts-check +const { spawn } = require('node:child_process'); +const { resolve } = require('node:path'); +const esbuild = require('esbuild'); + +const argv = process.argv.slice(2); +const watch = argv.includes('--watch'); +// Dev mode: build only the node target and (re)start the server after each successful build. +const runNode = argv.includes('--run-node'); +// Everything after `--` is forwarded verbatim to the spawned node server (e.g. `--port 5007`). +const dashDash = argv.indexOf('--'); +const serverArgs = dashDash >= 0 ? argv.slice(dashDash + 1) : []; + +const bundledDir = resolve(__dirname, '..', 'workflow-server-bundled'); +const bundledWebDir = resolve(__dirname, '..', 'workflow-server-bundled-web'); + +/** + * Reports the build progress and surfaces errors/warnings in a format that + * VS Code's `$esbuild-watch` problem matcher can pick up. + * @type {import('esbuild').Plugin} + */ +const esbuildProblemMatcherPlugin = { + name: 'esbuild-problem-matcher', + setup(build) { + build.onStart(() => { + console.log(`${watch ? '[watch] ' : ''}build started`); + }); + build.onEnd(result => { + result.errors.forEach(({ text, location }) => { + console.error(`✘ [ERROR] ${text}`); + if (location) { + console.error(` ${location.file}:${location.line}:${location.column}:`); + } + }); + console.log(`${watch ? '[watch] ' : ''}build finished`); + }); + } +}; + +/** @type {import('esbuild').BuildOptions} */ +const common = { + bundle: true, + // Minify one-shot production builds; keep watch/dev output readable and fast to rebuild. + // `sourcemap` still maps the (minified) output back to the original TypeScript sources. + minify: !watch, + // Preserve original class/function names through minification — GLSP's logger and error + // messages use `constructor.name` for labels, which would otherwise be mangled. + keepNames: true, + sourcemap: true, + logLevel: 'silent', + tsconfig: resolve(__dirname, 'tsconfig.json'), + plugins: [esbuildProblemMatcherPlugin] +}; + +/** @type {import('esbuild').BuildOptions} */ +const nodeConfig = { + ...common, + entryPoints: [resolve(__dirname, 'src/node/app.ts')], + outfile: resolve(bundledDir, 'wf-glsp-server-node.js'), + platform: 'node', + format: 'cjs', + target: 'node22', + // `ws` requires these optional native deps in a try/catch; keep them external so the + // graceful-fallback path works and esbuild does not error on the missing modules. + external: ['bufferutil', 'utf-8-validate'] +}; + +/** @type {import('esbuild').BuildOptions} */ +const webworkerConfig = { + ...common, + entryPoints: [resolve(__dirname, 'src/browser/app.ts')], + outfile: resolve(bundledWebDir, 'wf-glsp-server-webworker.js'), + platform: 'browser', + format: 'iife', + target: 'es2019', + // Honor the legacy `browser` package field + browser condition for transitive deps. + mainFields: ['browser', 'module', 'main'], + conditions: ['browser'] +}; + +/** Restart the bundled node server after every successful build. */ +function runNodePlugin() { + /** @type {import('node:child_process').ChildProcess | undefined} */ + let child; + const stop = () => child?.kill('SIGTERM'); + process.on('SIGINT', () => { + stop(); + process.exit(0); + }); + return { + name: 'run-node-server', + setup(build) { + build.onEnd(result => { + if (result.errors.length > 0) { + // Keep the last good server running so a broken build does not crash the loop. + return; + } + stop(); + child = spawn('node', ['--enable-source-maps', nodeConfig.outfile, ...serverArgs], { stdio: 'inherit' }); + }); + } + }; +} + +async function run(config) { + if (watch) { + const ctx = await esbuild.context(config); + await ctx.watch(); + } else { + await esbuild.build(config); + } +} + +async function main() { + if (runNode) { + await run({ ...nodeConfig, plugins: [esbuildProblemMatcherPlugin, runNodePlugin()] }); + } else { + await Promise.all([run(nodeConfig), run(webworkerConfig)]); + } +} + +main().catch(e => { + console.error(e); + process.exit(1); +}); diff --git a/examples/workflow-server/node.d.ts b/examples/workflow-server/node.d.ts new file mode 100644 index 0000000..98f277b --- /dev/null +++ b/examples/workflow-server/node.d.ts @@ -0,0 +1,16 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +export * from './lib/node/index'; diff --git a/examples/workflow-server/node.js b/examples/workflow-server/node.js new file mode 100644 index 0000000..a667cdd --- /dev/null +++ b/examples/workflow-server/node.js @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (c) 2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +'use strict'; + +module.exports = require('./lib/node/index'); diff --git a/examples/workflow-server/package.json b/examples/workflow-server/package.json new file mode 100644 index 0000000..02f3f38 --- /dev/null +++ b/examples/workflow-server/package.json @@ -0,0 +1,70 @@ +{ + "name": "@eclipse-glsp-examples/workflow-server", + "version": "2.8.0-next", + "description": "GLSP node server for the workflow example", + "keywords": [ + "eclipse", + "graphics", + "diagram", + "modeling", + "visualization", + "glsp", + "diagram editor" + ], + "homepage": "https://www.eclipse.org/glsp/", + "bugs": "https://github.com/eclipse-glsp/glsp/issues", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-glsp/glsp-server-node.git" + }, + "license": "(EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)", + "author": { + "name": "Eclipse GLSP" + }, + "contributors": [ + { + "name": "STMicroelectronics", + "url": "https://www.st.com/" + }, + { + "name": "Eclipse GLSP Project", + "email": "glsp-dev@eclipse.org", + "url": "https://projects.eclipse.org/projects/ecd.glsp" + } + ], + "main": "./lib/node/index.js", + "browser": { + "./lib/node/index.js": "./lib/browser/index.js" + }, + "types": "lib/index", + "files": [ + "lib", + "src", + "node.js", + "node.d.ts", + "browser.d.ts", + "browser.js" + ], + "scripts": { + "build": "tsc -b && pnpm bundle", + "bundle": "node esbuild.js", + "clean": "rimraf lib *.tsbuildinfo", + "dev": "node esbuild.js --watch --run-node -- --port 5007", + "dev:ws": "node esbuild.js --watch --run-node -- -w --port 8081", + "generate:index": "glsp generateIndex src/browser src/common src/node -s -f", + "lint": "eslint ./src" + }, + "dependencies": { + "@eclipse-glsp/layout-elk": "workspace:*", + "@eclipse-glsp/server": "workspace:*", + "@eclipse-glsp/server-mcp": "workspace:*", + "inversify": "^6.1.3", + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "esbuild": "~0.28.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/examples/workflow-server/src/browser/.indexignore b/examples/workflow-server/src/browser/.indexignore new file mode 100644 index 0000000..c998fd9 --- /dev/null +++ b/examples/workflow-server/src/browser/.indexignore @@ -0,0 +1,4 @@ +# The app.ts is the entrypoint when the workflow server is used bundled in an external node process +# It launches the server when imported so it has to be excluded from the default export to avoid unintended +# autostarts when the server package is directly integrated into a node backed (e.g. Theia) +app.ts \ No newline at end of file diff --git a/examples/workflow-server/src/browser/app.ts b/examples/workflow-server/src/browser/app.ts new file mode 100644 index 0000000..7140def --- /dev/null +++ b/examples/workflow-server/src/browser/app.ts @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import 'reflect-metadata'; + +import { configureELKLayoutModule } from '@eclipse-glsp/layout-elk'; +import { createAppModule, LogLevel, WorkerServerLauncher } from '@eclipse-glsp/server/browser'; +import { McpWorkerBridge } from '@eclipse-glsp/server-mcp/browser'; +import { Container } from 'inversify'; +import { WorkflowLayoutConfigurator } from '../common/layout/workflow-layout-configurator'; +import { WorkflowMcpDiagramModule } from '../common/mcp/workflow-mcp-diagram-module'; +import { WorkflowDiagramModule, WorkflowServerModule } from '../common/workflow-diagram-module'; +import { WorkflowMockModelStorage } from './mock-model-storage'; + +export async function launch(_argv?: string[]): Promise { + // Bridge must be created before any await so postMessages that arrive on the next event-loop tick aren't dropped. + const bridge = new McpWorkerBridge(); + + const appContainer = new Container(); + appContainer.load(createAppModule({ logLevel: LogLevel.info })); + + const launcher = appContainer.resolve(WorkerServerLauncher); + const elkLayoutModule = configureELKLayoutModule({ + algorithms: ['layered'], + layoutConfigurator: WorkflowLayoutConfigurator, + isWebWorker: true + }); + + const serverModule = new WorkflowServerModule().configureDiagramModule( + new WorkflowDiagramModule(() => WorkflowMockModelStorage), + elkLayoutModule, + new WorkflowMcpDiagramModule() + ); + + launcher.configure(serverModule, bridge.createServerModule()); + + // Resolves only on connection close — must be the last await. + await launcher.start({}); +} + +launch().catch(error => console.error('Error in workflow server launcher:', error)); diff --git a/examples/workflow-server/src/browser/example1.json b/examples/workflow-server/src/browser/example1.json new file mode 100644 index 0000000..6644aa0 --- /dev/null +++ b/examples/workflow-server/src/browser/example1.json @@ -0,0 +1,750 @@ +{ + "cssClasses": [], + "type": "graph", + "position": { + "x": 0, + "y": 0 + }, + "id": "sprotty", + "revision": 1, + "children": [ + { + "cssClasses": ["task", "automated"], + "type": "task:automated", + "id": "task_ChkTp", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 220, + "y": 150 + }, + "name": "ChkTp", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 82.4482421875, + "prefHeight": 30 + }, + "size": { + "width": 82.4482421875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_ChkTp_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 20.6171875, + "y": 13 + }, + "id": "task_ChkTp_label", + "text": "ChkTp", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 41.4482421875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": ["task", "manual"], + "type": "task:manual", + "id": "task_Push", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 70, + "y": 100 + }, + "name": "Push", + "taskType": "manual", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 72.921875, + "prefHeight": 30 + }, + "size": { + "width": 72.921875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_Push_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 15.9609375, + "y": 13 + }, + "id": "task_Push_label", + "text": "Push", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 31.921875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": ["forkOrJoin"], + "type": "activityNode:fork", + "id": "fork_1", + "position": { + "x": 170, + "y": 90 + }, + "nodeType": "forkNode", + "size": { + "width": 10, + "height": 50 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_Push_fork_1", + "sourceId": "task_Push", + "targetId": "fork_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_fork1_task_ChkTp", + "sourceId": "fork_1", + "targetId": "task_ChkTp", + "children": [] + }, + { + "cssClasses": ["task", "automated"], + "type": "task:automated", + "id": "task_ChkWt", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 220, + "y": 50 + }, + "name": "ChkWt", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 83.1103515625, + "prefHeight": 30 + }, + "size": { + "width": 83.1103515625, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_ChkWt_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 21, + "y": 13 + }, + "id": "task_ChkWt_label", + "text": "ChkWt", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 42.1103515625, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": ["decision"], + "type": "activityNode:decision", + "id": "decision_1", + "position": { + "x": 330, + "y": 50 + }, + "nodeType": "decisionNode", + "size": { + "width": 32, + "height": 32 + }, + "resizeLocations": ["top", "right", "bottom", "left"], + "children": [] + }, + { + "cssClasses": ["decision"], + "type": "activityNode:decision", + "id": "decision2", + "position": { + "x": 330, + "y": 150 + }, + "nodeType": "decisionNode", + "size": { + "width": 32, + "height": 32 + }, + "resizeLocations": ["top", "right", "bottom", "left"], + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_ChkWt_decision1", + "sourceId": "task_ChkWt", + "targetId": "decision_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_fork1_task_ChkWt", + "sourceId": "fork_1", + "targetId": "task_ChkWt", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_ChkTp_decision2", + "sourceId": "task_ChkTp", + "targetId": "decision2", + "children": [] + }, + { + "cssClasses": ["task", "automated"], + "type": "task:automated", + "id": "task_PreHeat", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 390, + "y": 180 + }, + "name": "PreHeat", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 92.46875, + "prefHeight": 30 + }, + "size": { + "width": 92.46875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_PreHeat_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 25.6796875, + "y": 13 + }, + "id": "task_PreHeat_label", + "text": "PreHeat", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 51.46875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": ["task", "automated"], + "type": "task:automated", + "id": "task_KeepTp", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 390, + "y": 130 + }, + "name": "KeepTp", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 90.248046875, + "prefHeight": 30 + }, + "size": { + "width": 90.248046875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_KeepTp_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 24.5234375, + "y": 13 + }, + "id": "task_KeepTp_label", + "text": "KeepTp", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 49.248046875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": ["task", "manual"], + "type": "task:manual", + "id": "task_RflWt", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 390, + "y": 20 + }, + "name": "RflWt", + "taskType": "manual", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 75.32421875, + "prefHeight": 30 + }, + "size": { + "width": 75.32421875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_RflWt_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 17.109375, + "y": 13 + }, + "id": "task_RflWt_label", + "text": "RflWt", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 34.32421875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": ["task", "automated"], + "type": "task:automated", + "id": "task_WtOK", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 390, + "y": 80 + }, + "name": "WtOK", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 78.9931640625, + "prefHeight": 30 + }, + "size": { + "width": 78.9931640625, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_WtOK_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 18.671875, + "y": 13 + }, + "id": "task_WtOK_label", + "text": "WtOK", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 37.9931640625, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": ["medium"], + "type": "edge:weighted", + "routingPoints": [], + "id": "edge_decision1_task_RflWt", + "sourceId": "decision_1", + "targetId": "task_RflWt", + "probability": "medium", + "children": [] + }, + { + "cssClasses": ["medium"], + "type": "edge:weighted", + "routingPoints": [], + "id": "edge_decision_1_task_WtOK", + "sourceId": "decision_1", + "targetId": "task_WtOK", + "probability": "medium", + "children": [] + }, + { + "cssClasses": ["medium"], + "type": "edge:weighted", + "routingPoints": [], + "id": "edege_decision_2_task_KeepTp", + "sourceId": "decision2", + "targetId": "task_KeepTp", + "probability": "medium", + "children": [] + }, + { + "cssClasses": ["medium"], + "type": "edge:weighted", + "routingPoints": [], + "id": "edege_decision2_task_PreHeat", + "sourceId": "decision2", + "targetId": "task_PreHeat", + "probability": "medium", + "children": [] + }, + { + "cssClasses": ["forkOrJoin"], + "type": "activityNode:join", + "id": "join_1", + "position": { + "x": 560, + "y": 90 + }, + "nodeType": "joinNode", + "size": { + "width": 10, + "height": 50 + }, + "children": [] + }, + { + "cssClasses": ["task", "automated"], + "type": "task:automated", + "id": "task_Brew", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 600, + "y": 100 + }, + "name": "Brew", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 73.20530319213867, + "prefHeight": 30 + }, + "size": { + "width": 73.20530319213867, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_Brew_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 15.953125, + "y": 13 + }, + "id": "task_Brew_label", + "text": "Brew", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 31.90625, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": ["merge"], + "type": "activityNode:merge", + "id": "merge_1", + "position": { + "x": 500, + "y": 50 + }, + "nodeType": "mergeNode", + "size": { + "width": 32, + "height": 32 + }, + "resizeLocations": ["top", "right", "bottom", "left"], + "children": [] + }, + { + "cssClasses": ["merge"], + "type": "activityNode:merge", + "id": "merge_2", + "position": { + "x": 500, + "y": 150 + }, + "nodeType": "mergeNode", + "size": { + "width": 32, + "height": 32 + }, + "resizeLocations": ["top", "right", "bottom", "left"], + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_RflWt_merge_1", + "sourceId": "task_RflWt", + "targetId": "merge_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_WTOK_merge_1", + "sourceId": "task_WtOK", + "targetId": "merge_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_KeepTp_merge_2", + "sourceId": "task_KeepTp", + "targetId": "merge_2", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edege_task_PreHeat_merge_2", + "sourceId": "task_PreHeat", + "targetId": "merge_2", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_merge_2_join_1", + "sourceId": "merge_2", + "targetId": "join_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_merge_1_join_1", + "sourceId": "merge_1", + "targetId": "join_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_join_1_task_Brew", + "sourceId": "join_1", + "targetId": "task_Brew", + "children": [] + } + ] +} diff --git a/examples/workflow-server/src/browser/index.ts b/examples/workflow-server/src/browser/index.ts new file mode 100644 index 0000000..8cdce46 --- /dev/null +++ b/examples/workflow-server/src/browser/index.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (c) 2022-2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +export * from './mock-model-storage'; +export * from './reexport'; diff --git a/examples/workflow-server/src/browser/mock-model-storage.ts b/examples/workflow-server/src/browser/mock-model-storage.ts new file mode 100644 index 0000000..06da839 --- /dev/null +++ b/examples/workflow-server/src/browser/mock-model-storage.ts @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { + GModelSerializer, + Logger, + MaybePromise, + ModelState, + RequestModelAction, + SaveModelAction, + SourceModelStorage +} from '@eclipse-glsp/server/browser'; +import { inject, injectable } from 'inversify'; +import wf from './example1.json'; + +@injectable() +export class WorkflowMockModelStorage implements SourceModelStorage { + @inject(Logger) + protected logger: Logger; + + @inject(GModelSerializer) + protected modelSerializer: GModelSerializer; + + @inject(ModelState) + protected modelState: ModelState; + + loadSourceModel(action: RequestModelAction): MaybePromise { + const root = this.modelSerializer.createRoot(wf); + this.modelState.updateRoot(root); + } + + saveSourceModel(action: SaveModelAction): MaybePromise { + this.logger.warn('Saving is not supported by this mock implementation'); + } +} diff --git a/examples/workflow-server/src/browser/reexport.ts b/examples/workflow-server/src/browser/reexport.ts new file mode 100644 index 0000000..61a2c52 --- /dev/null +++ b/examples/workflow-server/src/browser/reexport.ts @@ -0,0 +1,16 @@ +/******************************************************************************** + * Copyright (c) 2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +export * from '../common'; diff --git a/examples/workflow-server/src/common/graph-extension.ts b/examples/workflow-server/src/common/graph-extension.ts new file mode 100644 index 0000000..e6ba108 --- /dev/null +++ b/examples/workflow-server/src/common/graph-extension.ts @@ -0,0 +1,167 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { + Args, + ArgsUtil, + GCompartment, + GCompartmentBuilder, + GEdge, + GEdgeBuilder, + GLabel, + GLabelBuilder, + GNode, + GNodeBuilder +} from '@eclipse-glsp/server'; +import { ModelTypes } from './util/model-types'; + +export class ActivityNode extends GNode { + nodeType: string; + + static override builder(): ActivityNodeBuilder { + return new ActivityNodeBuilder(ActivityNode); + } +} + +export class ActivityNodeBuilder extends GNodeBuilder { + nodeType(nodeType: string): this { + this.proxy.nodeType = nodeType; + return this; + } +} + +export class TaskNode extends GNode { + name: string; + duration: number; + taskType: string; + references: string; + + static override builder(): TaskNodeBuilder { + return new TaskNodeBuilder(TaskNode).layout('vbox').addArgs(ArgsUtil.cornerRadius(5)).addCssClass('task'); + } +} + +export class TaskNodeBuilder extends GNodeBuilder { + name(name: string): this { + this.proxy.name = name; + return this; + } + + duration(duration: number): this { + this.proxy.duration = duration; + return this; + } + + taskType(tasktype: string): this { + this.proxy.taskType = tasktype; + return this; + } + + references(references: string): this { + this.proxy.references = references; + return this; + } + + children(): this { + return this; + } + + override build(): T { + this.layout('hbox').addLayoutOption('paddingRight', 10).add(this.createCompartmentIcon()).add(this.createCompartmentHeader()); + return super.build(); + } + + protected createCompartmentHeader(): GLabel { + return new GLabelBuilder(GLabel) + .type(ModelTypes.LABEL_HEADING) + .id(this.proxy.id + '_label') + .text(this.proxy.name) + .build(); + } + + protected createCompartmentIcon(): GCompartment { + return GCompartment.builder() + .id(this.proxy.id + '_icon') + .type(ModelTypes.ICON) + .build(); + } +} + +export class WeightedEdge extends GEdge { + probability?: string; + + static override builder(): WeightedEdgeBuilder { + return new WeightedEdgeBuilder(WeightedEdge).type(ModelTypes.WEIGHTED_EDGE); + } +} + +export class WeightedEdgeBuilder extends GEdgeBuilder { + probability(probability: string): this { + this.proxy.probability = probability; + return this; + } +} + +export class Category extends ActivityNode { + name: string; + + static override builder(): CategoryNodeBuilder { + return new CategoryNodeBuilder(Category) + .layout('vbox') + .addLayoutOptions({ hAlign: 'center', hGrab: false, vGrab: false }) + .addCssClass('category'); + } +} + +export class CategoryNodeBuilder extends ActivityNodeBuilder { + name(name: string): this { + this.proxy.name = name; + return this; + } + + children(): this { + this.proxy.children.push(this.createLabelCompartment()); + this.proxy.children.push(this.createStructCompartment()); + return this; + } + + protected createLabelCompartment(): GCompartment { + const layoutOptions: Args = {}; + return new GCompartmentBuilder(GCompartment) + .type(ModelTypes.COMP_HEADER) + .id(this.proxy.id + '_header') + .layout('hbox') + .addLayoutOptions(layoutOptions) + .add(this.createCompartmentHeader()) + .build(); + } + + protected createCompartmentHeader(): GLabel { + return new GLabelBuilder(GLabel) + .type(ModelTypes.LABEL_HEADING) + .id(this.proxy.id + '_label') + .text(this.proxy.name) + .build(); + } + + protected createStructCompartment(): GCompartment { + return new GCompartmentBuilder(GCompartment) + .type(ModelTypes.STRUCTURE) + .id(this.proxy.id + '_struct') + .layout('freeform') + .addLayoutOptions({ hAlign: 'left', hGrab: true, vGrab: true }) + .build(); + } +} diff --git a/examples/workflow-server/src/common/handler/create-activity-node-handler.ts b/examples/workflow-server/src/common/handler/create-activity-node-handler.ts new file mode 100644 index 0000000..6d620f1 --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-activity-node-handler.ts @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2022-2024 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { CreateNodeOperation, GNode, GhostElement, Point } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { ActivityNode, ActivityNodeBuilder } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; +import { CreateWorkflowNodeOperationHandler } from './create-workflow-node-operation-handler'; + +@injectable() +export abstract class CreateActivityNodeHandler extends CreateWorkflowNodeOperationHandler { + createNode(operation: CreateNodeOperation, relativeLocation?: Point): GNode | undefined { + return this.builder(relativeLocation).build(); + } + + protected builder(point: Point = Point.ORIGIN, elementTypeId = this.elementTypeIds[0]): ActivityNodeBuilder { + return ActivityNode.builder().position(point).type(elementTypeId).nodeType(ModelTypes.toNodeType(elementTypeId)); + } + + override createTriggerGhostElement(elementTypeId: string): GhostElement | undefined { + return { template: this.serializer.createSchema(this.builder(undefined, elementTypeId).build()) }; + } +} diff --git a/examples/workflow-server/src/common/handler/create-automated-task-handler.ts b/examples/workflow-server/src/common/handler/create-automated-task-handler.ts new file mode 100644 index 0000000..463924a --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-automated-task-handler.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { Point } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { TaskNodeBuilder } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; +import { CreateTaskHandler } from './create-task-handler'; + +@injectable() +export class CreateAutomatedTaskHandler extends CreateTaskHandler { + elementTypeIds = [ModelTypes.AUTOMATED_TASK]; + label = 'Automated Task'; + + protected override builder(point: Point | undefined): TaskNodeBuilder { + return super.builder(point).addCssClass('automated'); + } +} diff --git a/examples/workflow-server/src/common/handler/create-category-handler.ts b/examples/workflow-server/src/common/handler/create-category-handler.ts new file mode 100644 index 0000000..702af1d --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-category-handler.ts @@ -0,0 +1,41 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { ArgsUtil, CreateNodeOperation, GNode, GhostElement, Point } from '@eclipse-glsp/server'; +import { Category, CategoryNodeBuilder } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; +import { CreateWorkflowNodeOperationHandler } from './create-workflow-node-operation-handler'; + +export class CreateCategoryHandler extends CreateWorkflowNodeOperationHandler { + elementTypeIds = [ModelTypes.CATEGORY]; + label = 'Category'; + + createNode(operation: CreateNodeOperation, relativeLocation?: Point): GNode | undefined { + return this.builder(relativeLocation).build(); + } + + protected builder(point: Point = Point.ORIGIN, elementTypeId = this.elementTypeIds[0]): CategoryNodeBuilder { + return Category.builder() + .type(elementTypeId) + .position(point) + .name(this.label.replace(' ', '') + this.modelState.index.getAllByClass(Category).length) + .addArgs(ArgsUtil.cornerRadius(5)) + .children(); + } + + override createTriggerGhostElement(elementTypeId: string): GhostElement | undefined { + return { template: this.serializer.createSchema(this.builder(undefined, elementTypeId).build()), dynamic: true }; + } +} diff --git a/examples/workflow-server/src/common/handler/create-decision-node-handler.ts b/examples/workflow-server/src/common/handler/create-decision-node-handler.ts new file mode 100644 index 0000000..1bfa574 --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-decision-node-handler.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2022-2024 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { GResizeLocation, Point } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { ActivityNodeBuilder } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; +import { CreateActivityNodeHandler } from './create-activity-node-handler'; + +@injectable() +export class CreateDecisionNodeHandler extends CreateActivityNodeHandler { + elementTypeIds = [ModelTypes.DECISION_NODE]; + label = 'Decision Node'; + + protected override builder(point?: Point): ActivityNodeBuilder { + return super.builder(point).addCssClass('decision').resizeLocations(GResizeLocation.CROSS); + } +} diff --git a/examples/workflow-server/src/common/handler/create-edge-handler.ts b/examples/workflow-server/src/common/handler/create-edge-handler.ts new file mode 100644 index 0000000..93a5da7 --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-edge-handler.ts @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { DefaultTypes, GEdge, GEdgeBuilder, GModelCreateEdgeOperationHandler, GModelElement } from '@eclipse-glsp/server'; + +export class CreateEdgeHandler extends GModelCreateEdgeOperationHandler { + label = 'Edge'; + elementTypeIds = [DefaultTypes.EDGE]; + + createEdge(source: GModelElement, target: GModelElement): GEdge | undefined { + return new GEdgeBuilder(GEdge).sourceId(source.id).targetId(target.id).build(); + } +} diff --git a/examples/workflow-server/src/common/handler/create-fork-node-handler.ts b/examples/workflow-server/src/common/handler/create-fork-node-handler.ts new file mode 100644 index 0000000..3ddfc5d --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-fork-node-handler.ts @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { ModelTypes } from '../util/model-types'; +import { CreateForkOrJoinNodeHandler } from './create-fork-or-join-node-handler'; +import { injectable } from 'inversify'; + +@injectable() +export class CreateForkNodeHandler extends CreateForkOrJoinNodeHandler { + elementTypeIds = [ModelTypes.FORK_NODE]; + label = 'Fork Node'; +} diff --git a/examples/workflow-server/src/common/handler/create-fork-or-join-node-handler.ts b/examples/workflow-server/src/common/handler/create-fork-or-join-node-handler.ts new file mode 100644 index 0000000..828ebd0 --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-fork-or-join-node-handler.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { Point } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { ActivityNodeBuilder } from '../graph-extension'; +import { CreateActivityNodeHandler } from './create-activity-node-handler'; + +@injectable() +export abstract class CreateForkOrJoinNodeHandler extends CreateActivityNodeHandler { + protected override builder(point: Point | undefined): ActivityNodeBuilder { + return super.builder(point).addCssClass('forkOrJoin').size(10, 50); + } +} diff --git a/examples/workflow-server/src/common/handler/create-join-node-handler.ts b/examples/workflow-server/src/common/handler/create-join-node-handler.ts new file mode 100644 index 0000000..d106317 --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-join-node-handler.ts @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { ModelTypes } from '../util/model-types'; +import { CreateForkOrJoinNodeHandler } from './create-fork-or-join-node-handler'; +import { injectable } from 'inversify'; + +@injectable() +export class CreateJoinNodeHandler extends CreateForkOrJoinNodeHandler { + elementTypeIds = [ModelTypes.JOIN_NODE]; + label = 'Join Node'; +} diff --git a/examples/workflow-server/src/common/handler/create-manual-task-handler.ts b/examples/workflow-server/src/common/handler/create-manual-task-handler.ts new file mode 100644 index 0000000..6b016ee --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-manual-task-handler.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { Point } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { TaskNodeBuilder } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; +import { CreateTaskHandler } from './create-task-handler'; + +@injectable() +export class CreateManualTaskHandler extends CreateTaskHandler { + elementTypeIds = [ModelTypes.MANUAL_TASK]; + label = 'Manual Task'; + + protected override builder(point: Point | undefined): TaskNodeBuilder { + return super.builder(point).addCssClass('manual'); + } +} diff --git a/examples/workflow-server/src/common/handler/create-merge-node-handler.ts b/examples/workflow-server/src/common/handler/create-merge-node-handler.ts new file mode 100644 index 0000000..9330d4b --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-merge-node-handler.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2022-2024 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { GResizeLocation, Point } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { ActivityNodeBuilder } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; +import { CreateActivityNodeHandler } from './create-activity-node-handler'; + +@injectable() +export class CreateMergeNodeHandler extends CreateActivityNodeHandler { + elementTypeIds = [ModelTypes.MERGE_NODE]; + label = 'Merge Node'; + + protected override builder(point: Point | undefined): ActivityNodeBuilder { + return super.builder(point).addCssClass('merge').resizeLocations(GResizeLocation.CROSS); + } +} diff --git a/examples/workflow-server/src/common/handler/create-task-handler.ts b/examples/workflow-server/src/common/handler/create-task-handler.ts new file mode 100644 index 0000000..9e95bca --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-task-handler.ts @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { CreateNodeOperation, GhostElement, GNode, Point } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { TaskNode, TaskNodeBuilder } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; +import { CreateWorkflowNodeOperationHandler } from './create-workflow-node-operation-handler'; + +@injectable() +export abstract class CreateTaskHandler extends CreateWorkflowNodeOperationHandler { + createNode(operation: CreateNodeOperation, relativeLocation?: Point): GNode | undefined { + return this.builder(relativeLocation).build(); + } + + protected builder(point: Point = Point.ORIGIN, elementTypeId = this.elementTypeIds[0]): TaskNodeBuilder { + return TaskNode.builder() + .position(point ?? Point.ORIGIN) + .name(this.label.replace(' ', '') + this.modelState.index.getAllByClass(TaskNode).length) + .type(elementTypeId) + .taskType(ModelTypes.toNodeType(elementTypeId)) + .children(); + } + + override createTriggerGhostElement(elementTypeId: string): GhostElement | undefined { + return { template: this.serializer.createSchema(this.builder(undefined, elementTypeId).build()), dynamic: true }; + } +} diff --git a/examples/workflow-server/src/common/handler/create-weighted-edge-handler.ts b/examples/workflow-server/src/common/handler/create-weighted-edge-handler.ts new file mode 100644 index 0000000..a4db956 --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-weighted-edge-handler.ts @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { GEdge, GModelCreateEdgeOperationHandler, GModelElement } from '@eclipse-glsp/server'; +import { WeightedEdge } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; + +export class CreateWeightedEdgeHandler extends GModelCreateEdgeOperationHandler { + elementTypeIds = [ModelTypes.WEIGHTED_EDGE]; + label = 'Weighted edge'; + + createEdge(source: GModelElement, target: GModelElement): GEdge | undefined { + return WeightedEdge.builder().sourceId(source.id).targetId(target.id).probability('medium').addCssClass('medium').build(); + } +} diff --git a/examples/workflow-server/src/common/handler/create-workflow-node-operation-handler.ts b/examples/workflow-server/src/common/handler/create-workflow-node-operation-handler.ts new file mode 100644 index 0000000..18ff85f --- /dev/null +++ b/examples/workflow-server/src/common/handler/create-workflow-node-operation-handler.ts @@ -0,0 +1,56 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { + CreateNodeOperation, + GCompartment, + GModelCreateNodeOperationHandler, + GModelElement, + ModelState, + Point +} from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; +import { Category } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; +import { GridSnapper } from './grid-snapper'; + +@injectable() +export abstract class CreateWorkflowNodeOperationHandler extends GModelCreateNodeOperationHandler { + @inject(ModelState) + protected override modelState: ModelState; + + override getLocation(operation: CreateNodeOperation): Point | undefined { + return GridSnapper.snap(operation.location); + } + + override getContainer(operation: CreateNodeOperation): GModelElement | undefined { + const container = super.getContainer(operation); + + if (container instanceof Category) { + const structComp = this.getCategoryCompartment(container); + if (structComp) { + return structComp; + } + } + return container; + } + + getCategoryCompartment(category: Category): GCompartment | undefined { + return category.children + .filter(child => child instanceof GCompartment) + .map(child => child as GCompartment) + .find(comp => ModelTypes.STRUCTURE === comp.type); + } +} diff --git a/examples/workflow-server/src/common/handler/grid-snapper.ts b/examples/workflow-server/src/common/handler/grid-snapper.ts new file mode 100644 index 0000000..b9a7388 --- /dev/null +++ b/examples/workflow-server/src/common/handler/grid-snapper.ts @@ -0,0 +1,32 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { Point } from '@eclipse-glsp/server'; + +export class GridSnapper { + public static GRID_X = 10.0; + public static GRID_Y = 10.0; + + public static snap(originalPoint: Point | undefined): Point | undefined { + if (originalPoint) { + return { + x: Math.round(originalPoint.x / this.GRID_X) * this.GRID_X, + y: Math.round(originalPoint.y / this.GRID_Y) * this.GRID_Y + }; + } else { + return undefined; + } + } +} diff --git a/examples/workflow-server/src/common/index.ts b/examples/workflow-server/src/common/index.ts new file mode 100644 index 0000000..452c517 --- /dev/null +++ b/examples/workflow-server/src/common/index.ts @@ -0,0 +1,54 @@ +/******************************************************************************** + * Copyright (c) 2023-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +export * from './graph-extension'; +export * from './handler/create-activity-node-handler'; +export * from './handler/create-automated-task-handler'; +export * from './handler/create-category-handler'; +export * from './handler/create-decision-node-handler'; +export * from './handler/create-edge-handler'; +export * from './handler/create-fork-node-handler'; +export * from './handler/create-fork-or-join-node-handler'; +export * from './handler/create-join-node-handler'; +export * from './handler/create-manual-task-handler'; +export * from './handler/create-merge-node-handler'; +export * from './handler/create-task-handler'; +export * from './handler/create-weighted-edge-handler'; +export * from './handler/create-workflow-node-operation-handler'; +export * from './handler/grid-snapper'; +export * from './labeledit/workflow-label-edit-validator'; +export * from './layout/workflow-layout-configurator'; +export * from './marker/workflow-model-validator'; +export * from './mcp/workflow-element-types-provider'; +export * from './mcp/workflow-mcp-diagram-module'; +export * from './mcp/workflow-mcp-label-provider'; +export * from './mcp/workflow-mcp-model-serializer'; +export * from './model/workflow-navigation-target-resolver'; +export * from './provider/abstract-next-or-previous-navigation-target-provider'; +export * from './provider/next-node-navigation-target-provider'; +export * from './provider/node-documentation-navigation-target-provider'; +export * from './provider/previous-node-navigation-target-provider'; +export * from './provider/workflow-command-palette-action-provider'; +export * from './provider/workflow-context-menu-item-provider'; +export * from './taskedit/edit-task-operation-handler'; +export * from './taskedit/task-edit-context-provider'; +export * from './taskedit/task-edit-validator'; +export * from './util/model-types'; +export * from './workflow-diagram-configuration'; +export * from './workflow-diagram-module'; +export * from './workflow-edge-creation-checker'; +export * from './workflow-glsp-server'; +export * from './workflow-popup-factory'; diff --git a/examples/workflow-server/src/common/labeledit/workflow-label-edit-validator.ts b/examples/workflow-server/src/common/labeledit/workflow-label-edit-validator.ts new file mode 100644 index 0000000..6359ffc --- /dev/null +++ b/examples/workflow-server/src/common/labeledit/workflow-label-edit-validator.ts @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { GModelElement, LabelEditValidator, ModelState, ValidationStatus } from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; +import { TaskNode } from '../graph-extension'; + +@injectable() +export class WorkflowLabelEditValidator implements LabelEditValidator { + @inject(ModelState) + protected modelState: ModelState; + + validate(label: string, element: GModelElement): ValidationStatus { + if (label.length < 1) { + return { severity: ValidationStatus.Severity.ERROR, message: 'Name must not be empty' }; + } + const taskNodes = this.modelState.index.getAllByClass(TaskNode); + const hasDuplicate = taskNodes + .filter(e => !(e.id === element.id)) + .map(task => task.name) + .some(name => name === label); + if (hasDuplicate) { + return { severity: ValidationStatus.Severity.WARNING, message: 'Name should be unique' }; + } + return { severity: ValidationStatus.Severity.OK }; + } +} diff --git a/examples/workflow-server/src/common/layout/workflow-layout-configurator.ts b/examples/workflow-server/src/common/layout/workflow-layout-configurator.ts new file mode 100644 index 0000000..200bcf9 --- /dev/null +++ b/examples/workflow-server/src/common/layout/workflow-layout-configurator.ts @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { AbstractLayoutConfigurator, LayoutOptions } from '@eclipse-glsp/layout-elk'; +import { GGraph } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; + +@injectable() +export class WorkflowLayoutConfigurator extends AbstractLayoutConfigurator { + protected override graphOptions(graph: GGraph): LayoutOptions | undefined { + return { + 'elk.algorithm': 'layered' + }; + } +} diff --git a/examples/workflow-server/src/common/marker/workflow-model-validator.ts b/examples/workflow-server/src/common/marker/workflow-model-validator.ts new file mode 100644 index 0000000..05b9949 --- /dev/null +++ b/examples/workflow-server/src/common/marker/workflow-model-validator.ts @@ -0,0 +1,138 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { AbstractModelValidator, GCompartment, GLabel, GModelElement, Marker, MarkerKind, ModelState } from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; +import { ActivityNode, TaskNode } from '../graph-extension'; + +@injectable() +export class WorkflowModelValidator extends AbstractModelValidator { + @inject(ModelState) + protected readonly modelState: ModelState; + + override doLiveValidation(element: GModelElement): Marker[] { + const markers: Marker[] = []; + if (element instanceof ActivityNode) { + if (element.nodeType === 'decisionNode') { + markers.push(...this.validateDecisionNode(element)); + } else if (element.nodeType === 'mergeNode') { + markers.push(...this.validateMergeNode(element)); + } + } + return markers; + } + + override doBatchValidation(element: GModelElement): Marker[] { + const markers: Marker[] = []; + if (element instanceof TaskNode) { + markers.push(...this.validateTaskNode(element)); + } + return markers; + } + + protected validateTaskNode(taskNode: TaskNode): Marker[] { + const markers: Marker[] = []; + const automated = this.validateTaskNode_isAutomated(taskNode); + if (automated) { + markers.push(automated); + } + const upperCase = this.validateTaskNode_labelStartsUpperCase(taskNode); + if (upperCase) { + markers.push(upperCase); + } + return markers; + } + + protected validateTaskNode_isAutomated(taskNode: TaskNode): Marker | undefined { + if (taskNode.taskType === 'automated') { + return { kind: MarkerKind.INFO, description: 'This is an automated task', elementId: taskNode.id, label: 'Automated task' }; + } + return undefined; + } + + protected validateTaskNode_labelStartsUpperCase(taskNode: TaskNode): Marker | undefined { + const gCompartment = taskNode.children.find(child => child instanceof GCompartment); + if (gCompartment) { + const gLabels = gCompartment.children.filter(child => child instanceof GLabel).map(element => element as GLabel); + if (gLabels.length > 0 && gLabels[0].text.charAt(0) === gLabels[0].text.charAt(0).toLowerCase()) { + return { + kind: MarkerKind.WARNING, + description: 'Task node names should start with upper case letters', + elementId: taskNode.id, + label: 'Task node label in upper case' + }; + } + } + return undefined; + } + + protected validateDecisionNode(decisionNode: ActivityNode): Marker[] { + const markers: Marker[] = []; + const oneIncoming = this.validateDecisionNode_hasOneIncomingEdge(decisionNode); + if (oneIncoming) { + markers.push(oneIncoming); + } + return markers; + } + + protected validateDecisionNode_hasOneIncomingEdge(decisionNode: ActivityNode): Marker | undefined { + const edges = this.modelState.index.getIncomingEdges(decisionNode); + if (edges.length > 1) { + return { + kind: MarkerKind.ERROR, + description: 'Decision node may only have one incoming edge.', + elementId: decisionNode.id, + label: 'Too many incoming edges' + }; + } else if (edges.length === 0) { + return { + kind: MarkerKind.ERROR, + description: 'Decision node must have one incoming edge.', + elementId: decisionNode.id, + label: 'Missing incoming edge' + }; + } + return undefined; + } + + protected validateMergeNode(mergeNode: ActivityNode): Marker[] { + const markers: Marker[] = []; + const oneOutgoing = this.validateMergeNode_hasOneOutgoingEdge(mergeNode); + if (oneOutgoing) { + markers.push(oneOutgoing); + } + return markers; + } + + protected validateMergeNode_hasOneOutgoingEdge(mergeNode: ActivityNode): Marker | undefined { + const edges = this.modelState.index.getOutgoingEdges(mergeNode); + if (edges.length > 1) { + return { + kind: MarkerKind.ERROR, + description: 'Merge node may only have one outgoing edge.', + elementId: mergeNode.id, + label: 'Too many outgoing edges' + }; + } else if (edges.length === 0) { + return { + kind: MarkerKind.ERROR, + description: 'Merge node must have one outgoing edge.', + elementId: mergeNode.id, + label: 'Missing outgoing edge' + }; + } + return undefined; + } +} diff --git a/examples/workflow-server/src/common/mcp/workflow-element-types-provider.ts b/examples/workflow-server/src/common/mcp/workflow-element-types-provider.ts new file mode 100644 index 0000000..19696e6 --- /dev/null +++ b/examples/workflow-server/src/common/mcp/workflow-element-types-provider.ts @@ -0,0 +1,54 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DefaultTypes } from '@eclipse-glsp/server'; +import { ElementTypeEntry, ElementTypes, ElementTypesProvider } from '@eclipse-glsp/server-mcp'; +import { injectable } from 'inversify'; +import { ModelTypes } from '../util/model-types'; + +const NODE_TYPES: ElementTypeEntry[] = [ + { id: ModelTypes.AUTOMATED_TASK, label: 'Automated Task', description: 'Task without human input', acceptsText: true }, + { id: ModelTypes.MANUAL_TASK, label: 'Manual Task', description: 'Task done by a human', acceptsText: true }, + { id: ModelTypes.JOIN_NODE, label: 'Join Node', description: 'Gateway that merges parallel flows', acceptsText: false }, + { id: ModelTypes.FORK_NODE, label: 'Fork Node', description: 'Gateway that splits into parallel flows', acceptsText: false }, + { id: ModelTypes.MERGE_NODE, label: 'Merge Node', description: 'Gateway that merges alternative flows', acceptsText: false }, + { id: ModelTypes.DECISION_NODE, label: 'Decision Node', description: 'Gateway that splits into alternative flows', acceptsText: false }, + { id: ModelTypes.CATEGORY, label: 'Category', description: 'Container node that groups other elements', acceptsText: true } +]; + +const EDGE_TYPES: ElementTypeEntry[] = [ + { id: DefaultTypes.EDGE, label: 'Edge', description: 'Standard control flow edge', acceptsText: false }, + { + id: ModelTypes.WEIGHTED_EDGE, + label: 'Weighted Edge', + description: 'Edge that indicates a weighted probability. Typically used with a Decision Node.', + acceptsText: false + } +]; + +/** + * Workflow-specific {@link ElementTypesProvider}. Returns the constant set of creatable types + * with richer LLM-facing fields (`description`, `acceptsText`) than the default registry-scrape + * impl can infer. Bound on the workflow MCP diagram module via `bindElementTypesProvider()`; + * the standard `element-types` tool handler exposes the full entries via `structuredContent` + * (with a short summary in the text content) — no handler rebind needed. + */ +@injectable() +export class WorkflowElementTypesProvider implements ElementTypesProvider { + get(): ElementTypes { + return { nodeTypes: NODE_TYPES, edgeTypes: EDGE_TYPES }; + } +} diff --git a/examples/workflow-server/src/common/mcp/workflow-mcp-diagram-module.ts b/examples/workflow-server/src/common/mcp/workflow-mcp-diagram-module.ts new file mode 100644 index 0000000..70252b9 --- /dev/null +++ b/examples/workflow-server/src/common/mcp/workflow-mcp-diagram-module.ts @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { BindingTarget } from '@eclipse-glsp/server'; +import { DefaultMcpDiagramModule, ElementTypesProvider, McpLabelProvider, McpModelSerializer } from '@eclipse-glsp/server-mcp'; +import { WorkflowElementTypesProvider } from './workflow-element-types-provider'; +import { WorkflowMcpLabelProvider } from './workflow-mcp-label-provider'; +import { WorkflowMcpModelSerializer } from './workflow-mcp-model-serializer'; + +/** + * Workflow-specific diagram-scope MCP module — swaps in the workflow-aware + * {@link McpLabelProvider}, {@link McpModelSerializer}, and {@link ElementTypesProvider}. + */ +export class WorkflowMcpDiagramModule extends DefaultMcpDiagramModule { + protected override bindLabelProvider(): BindingTarget { + return WorkflowMcpLabelProvider; + } + + protected override bindModelSerializer(): BindingTarget { + return WorkflowMcpModelSerializer; + } + + protected override bindElementTypesProvider(): BindingTarget { + return WorkflowElementTypesProvider; + } +} diff --git a/examples/workflow-server/src/common/mcp/workflow-mcp-label-provider.ts b/examples/workflow-server/src/common/mcp/workflow-mcp-label-provider.ts new file mode 100644 index 0000000..8b2ef55 --- /dev/null +++ b/examples/workflow-server/src/common/mcp/workflow-mcp-label-provider.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { GLabel, GModelElement } from '@eclipse-glsp/server'; +import { DefaultMcpLabelProvider } from '@eclipse-glsp/server-mcp'; +import { injectable } from 'inversify'; +import { ModelTypes } from '../util/model-types'; + +/** + * Workflow-specific {@link DefaultMcpLabelProvider}: category labels are nested inside a + * compartment-header child rather than directly under the category, so the default + * direct-child lookup misses them. Other workflow element types fall through to the + * inherited default. + */ +@injectable() +export class WorkflowMcpLabelProvider extends DefaultMcpLabelProvider { + override getLabel(element: GModelElement): GLabel | undefined { + if (element.type === ModelTypes.CATEGORY) { + const header = element.children.find(child => child.type === ModelTypes.COMP_HEADER); + return header?.children.find((child): child is GLabel => child instanceof GLabel); + } + return super.getLabel(element); + } +} diff --git a/examples/workflow-server/src/common/mcp/workflow-mcp-model-serializer.ts b/examples/workflow-server/src/common/mcp/workflow-mcp-model-serializer.ts new file mode 100644 index 0000000..cce18bf --- /dev/null +++ b/examples/workflow-server/src/common/mcp/workflow-mcp-model-serializer.ts @@ -0,0 +1,186 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DefaultTypes, Dimension, GModelElement } from '@eclipse-glsp/server'; +import { MarkdownMcpModelSerializer, SerializedElement } from '@eclipse-glsp/server-mcp'; +import { injectable } from 'inversify'; +import { ModelTypes } from '../util/model-types'; + +/** + * As compared to the {@link MarkdownMcpModelSerializer}, this is a specific implementation and we + * know not only the structure of our graph but also each relevant attribute. This enables us to + * order them semantically so the produced serialization makes more sense if read with semantics + * mind. As LLMs (i.e., the MCP clients) work semantically, this is superior to a random ordering. + * Furthermore, including only the relevant information without redundancies decreases context size. + */ +@injectable() +export class WorkflowMcpModelSerializer extends MarkdownMcpModelSerializer { + override prepareElement(element: GModelElement): Record { + // Pass the live parent's id so the input element gets `parentId` set even when the + // spread inside `flattenStructure` doesn't carry the (typically non-enumerable) `parent` + // reference. Without this, `query-elements` inspect on a single leaf drops `parentId`, + // creating an inconsistency with `diagram-model`. + const elements = this.flattenStructure(element as unknown as SerializedElement, element.parent?.id); + + // Define the order of keys + const result: Record = { + [DefaultTypes.GRAPH]: [], + [ModelTypes.CATEGORY]: [], + [ModelTypes.AUTOMATED_TASK]: [], + [ModelTypes.MANUAL_TASK]: [], + [ModelTypes.FORK_NODE]: [], + [ModelTypes.JOIN_NODE]: [], + [ModelTypes.DECISION_NODE]: [], + [ModelTypes.MERGE_NODE]: [], + [DefaultTypes.EDGE]: [], + [ModelTypes.WEIGHTED_EDGE]: [] + }; + elements.forEach(serialized => { + this.combinePositionAndSize(serialized); + + const adjustedElement = this.adjustElement(serialized); + if (!adjustedElement) { + return; + } + + const type = serialized.type; + if (typeof type === 'string' && result[type]) { + result[type].push(adjustedElement); + } + }); + + return result; + } + + private adjustElement(element: SerializedElement): SerializedElement | undefined { + const type = element.type; + switch (type) { + case ModelTypes.AUTOMATED_TASK: + case ModelTypes.MANUAL_TASK: { + const children = Array.isArray(element.children) ? (element.children as SerializedElement[]) : []; + const label = children.find(child => child.type === ModelTypes.LABEL_HEADING); + if (!label || !Dimension.is(label.size)) { + return undefined; + } + + // For tasks, the only content with impact on element size is the label + // Therefore, all other factors get integrated into the label size for the AI to do proper resizing operations + const labelSize = { + // 10px padding right, 31px padding left (incl. icon) + width: Math.trunc(label.size.width + 10 + 31), + // 7px padding top and bottom each + height: Math.trunc(label.size.height + 14) + }; + + // If the task lives inside a `STRUCTURE` (a layout-only wrapper), report the + // structure's parent as the logical parent. Falls back to the direct `parentId` + // when the task is at the root or the structure has no parent. + const liveParent = (element as { parent?: GModelElement }).parent; + const parentId = liveParent?.type === ModelTypes.STRUCTURE ? (liveParent.parent?.id ?? element.parentId) : element.parentId; + return { + id: element.id, + type, + position: element.position, + size: element.size, + bounds: element.bounds, + label: label.text, + labelSize, + parentId + }; + } + case ModelTypes.CATEGORY: { + const children = Array.isArray(element.children) ? (element.children as SerializedElement[]) : []; + const header = children.find(child => child.type === ModelTypes.COMP_HEADER); + const headerChildren = header && Array.isArray(header.children) ? (header.children as SerializedElement[]) : []; + const label = headerChildren.find(child => child.type === ModelTypes.LABEL_HEADING); + if (!label || !Dimension.is(label.size)) { + return undefined; + } + + const labelSize = { + width: Math.trunc(label.size.width + 20), + height: Math.trunc(label.size.height + 20) + }; + + // `combinePositionAndSize` (in MarkdownMcpModelSerializer) omits `size`/`bounds` + // when an element has no explicit geometry yet, so the LLM doesn't try to "fix" + // placeholder bounds. Categories normally do have an explicit size in the model, + // but if a freshly created (or otherwise unsized) category lands here, skip the + // derived `usableSpaceSize` rather than crashing on `element.size.width`. + const usableSpaceSize = Dimension.is(element.size) + ? { + width: Math.trunc(Math.max(0, element.size.width - 10)), + height: Math.trunc(Math.max(0, element.size.height - labelSize.height - 10)) + } + : undefined; + + return { + id: element.id, + type, + isContainer: true, + position: element.position, + size: element.size, + bounds: element.bounds, + label: label.text, + labelSize, + usableSpaceSize, + parentId: element.parentId + }; + } + case ModelTypes.JOIN_NODE: + case ModelTypes.MERGE_NODE: + case ModelTypes.DECISION_NODE: + case ModelTypes.FORK_NODE: { + return { + id: element.id, + type, + position: element.position, + size: element.size, + bounds: element.bounds, + parentId: element.parentId + }; + } + case DefaultTypes.EDGE: { + return { + id: element.id, + type, + sourceId: element.sourceId, + targetId: element.targetId, + parentId: element.parentId + }; + } + case ModelTypes.WEIGHTED_EDGE: { + return { + id: element.id, + type, + sourceId: element.sourceId, + targetId: element.targetId, + probability: element.probability, + parentId: element.parentId + }; + } + case DefaultTypes.GRAPH: { + return { + id: element.id, + type, + isContainer: true + }; + } + default: + return undefined; + } + } +} diff --git a/examples/workflow-server/src/common/model/workflow-navigation-target-resolver.ts b/examples/workflow-server/src/common/model/workflow-navigation-target-resolver.ts new file mode 100644 index 0000000..4cb4c43 --- /dev/null +++ b/examples/workflow-server/src/common/model/workflow-navigation-target-resolver.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { ModelState, NavigationTarget, NavigationTargetResolution, NavigationTargetResolver } from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; +import { TaskNode } from '../graph-extension'; + +@injectable() +export class WorkflowNavigationTargetResolver extends NavigationTargetResolver { + @inject(ModelState) + protected readonly modelState: ModelState; + + async resolve(navigationTarget: NavigationTarget): Promise { + if (navigationTarget.args && navigationTarget.args['name']) { + const name = navigationTarget.args['name']; + const taskNodes = this.modelState.index.getAllByClass(TaskNode); + const element = taskNodes.find(node => name === node.name); + if (element) { + return new NavigationTargetResolution([element.id]); + } + return new NavigationTargetResolution([], this.createArgsWithWarning(`Couldn't find element with name ${name}`)); + } + return NavigationTargetResolution.EMPTY; + } +} diff --git a/examples/workflow-server/src/common/provider/abstract-next-or-previous-navigation-target-provider.ts b/examples/workflow-server/src/common/provider/abstract-next-or-previous-navigation-target-provider.ts new file mode 100644 index 0000000..798b69e --- /dev/null +++ b/examples/workflow-server/src/common/provider/abstract-next-or-previous-navigation-target-provider.ts @@ -0,0 +1,51 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { Args, EditorContext, GEdge, ModelState, NavigationTarget, NavigationTargetProvider } from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; +import { TaskNode } from '../graph-extension'; + +@injectable() +export abstract class AbstractNextOrPreviousNavigationTargetProvider implements NavigationTargetProvider { + abstract targetTypeId: string; + + @inject(ModelState) + protected readonly modelState: ModelState; + + getTargets(editorContext: EditorContext): NavigationTarget[] { + const sourceUri = this.modelState.sourceUri; + const edges: GEdge[] = []; + editorContext.selectedElementIds + .map(id => this.modelState.index.get(id)) + .filter(e => e instanceof TaskNode) + .map(e => e as TaskNode) + .map(taskNode => edges.push(...this.getEdges(taskNode))); + return edges.map(edge => this.getSourceOrTarget(edge)).map(id => this.createNavigationTarget(sourceUri, id)); + } + + protected createNavigationTarget(sourceURI: string | undefined, id: string): NavigationTarget { + return { uri: sourceURI ?? '', args: this.createElementIdMap(id) }; + } + + protected createElementIdMap(id: string): Args { + const map: Args = {}; + map[NavigationTarget.ELEMENT_IDS] = id; + return map; + } + + protected abstract getEdges(taskNode: TaskNode): GEdge[]; + + protected abstract getSourceOrTarget(edge: GEdge): string; +} diff --git a/examples/workflow-server/src/common/provider/next-node-navigation-target-provider.ts b/examples/workflow-server/src/common/provider/next-node-navigation-target-provider.ts new file mode 100644 index 0000000..f9a15e7 --- /dev/null +++ b/examples/workflow-server/src/common/provider/next-node-navigation-target-provider.ts @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { GEdge } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { TaskNode } from '../graph-extension'; +import { AbstractNextOrPreviousNavigationTargetProvider } from './abstract-next-or-previous-navigation-target-provider'; + +@injectable() +export class NextNodeNavigationTargetProvider extends AbstractNextOrPreviousNavigationTargetProvider { + targetTypeId = 'next'; + protected getEdges(taskNode: TaskNode): GEdge[] { + return this.modelState.index.getOutgoingEdges(taskNode); + } + + protected getSourceOrTarget(edge: GEdge): string { + return edge.targetId; + } +} diff --git a/examples/workflow-server/src/common/provider/node-documentation-navigation-target-provider.ts b/examples/workflow-server/src/common/provider/node-documentation-navigation-target-provider.ts new file mode 100644 index 0000000..1a4aa1e --- /dev/null +++ b/examples/workflow-server/src/common/provider/node-documentation-navigation-target-provider.ts @@ -0,0 +1,57 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { + Args, + EditorContext, + GLabel, + JsonOpenerOptions, + ModelState, + NavigationTarget, + NavigationTargetProvider +} from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; +import { TaskNode } from '../graph-extension'; + +@injectable() +export class NodeDocumentationNavigationTargetProvider implements NavigationTargetProvider { + targetTypeId = 'documentation'; + + @inject(ModelState) + protected readonly modelState: ModelState; + + getTargets(editorContext: EditorContext): NavigationTarget[] { + if (editorContext.selectedElementIds.length === 1) { + const taskNode = this.modelState.index.findByClass(editorContext.selectedElementIds[0], TaskNode); + if (!taskNode || !taskNode.children.some(child => child instanceof GLabel && child.text === 'Push')) { + return []; + } + + const sourceUri = this.modelState.sourceUri; + if (!sourceUri) { + return []; + } + + const docUri = sourceUri.replace('.wf', '.md'); + const args: Args = {}; + args['jsonOpenerOptions'] = new JsonOpenerOptions({ + start: { line: 2, character: 3 }, + end: { line: 2, character: 7 } + }).toJson(); + return [{ uri: docUri, args: args }]; + } + return []; + } +} diff --git a/examples/workflow-server/src/common/provider/previous-node-navigation-target-provider.ts b/examples/workflow-server/src/common/provider/previous-node-navigation-target-provider.ts new file mode 100644 index 0000000..40df2d3 --- /dev/null +++ b/examples/workflow-server/src/common/provider/previous-node-navigation-target-provider.ts @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { GEdge } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { TaskNode } from '../graph-extension'; +import { AbstractNextOrPreviousNavigationTargetProvider } from './abstract-next-or-previous-navigation-target-provider'; + +@injectable() +export class PreviousNodeNavigationTargetProvider extends AbstractNextOrPreviousNavigationTargetProvider { + targetTypeId = 'previous'; + protected getEdges(taskNode: TaskNode): GEdge[] { + return this.modelState.index.getIncomingEdges(taskNode); + } + + protected getSourceOrTarget(edge: GEdge): string { + return edge.sourceId; + } +} diff --git a/examples/workflow-server/src/common/provider/workflow-command-palette-action-provider.ts b/examples/workflow-server/src/common/provider/workflow-command-palette-action-provider.ts new file mode 100644 index 0000000..e78f44c --- /dev/null +++ b/examples/workflow-server/src/common/provider/workflow-command-palette-action-provider.ts @@ -0,0 +1,142 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { + Args, + CommandPaletteActionProvider, + CreateEdgeOperation, + CreateNodeOperation, + DefaultTypes, + DeleteElementOperation, + GModelElement, + GNode, + LabeledAction, + ModelState, + Point +} from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; +import { TaskNode } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; + +@injectable() +export class WorkflowCommandPaletteActionProvider extends CommandPaletteActionProvider { + @inject(ModelState) + protected override modelState: ModelState; + + getPaletteActions(selectedElementIds: string[], selectedElements: GModelElement[], position: Point, args?: Args): LabeledAction[] { + const actions: LabeledAction[] = []; + if (this.modelState.isReadonly) { + return actions; + } + const index = this.modelState.index; + // Create actions + const location = position ?? Point.ORIGIN; + actions.push( + { + label: 'Create Automated Task', + actions: [CreateNodeOperation.create(ModelTypes.AUTOMATED_TASK, { location })], + icon: 'fa-plus-square' + }, + { + label: 'Create Manual Task', + actions: [CreateNodeOperation.create(ModelTypes.MANUAL_TASK, { location })], + icon: 'fa-plus-square' + }, + { + label: 'Create Merge Node', + actions: [CreateNodeOperation.create(ModelTypes.MERGE_NODE, { location })], + icon: 'fa-plus-square' + }, + { + label: 'Create Decision Node', + actions: [CreateNodeOperation.create(ModelTypes.DECISION_NODE, { location })], + icon: 'fa-plus-square' + }, + { + label: 'Create Category', + actions: [CreateNodeOperation.create(ModelTypes.CATEGORY, { location })], + icon: 'fa-plus-square' + } + ); + // Create edge action between two nodes + if (selectedElements.length === 1) { + const element = selectedElements[0]; + if (element instanceof GNode) { + actions.push(...this.createEdgeActions(element, index.getAllByClass(TaskNode))); + } + } else if (selectedElements.length === 2) { + const source = selectedElements[0]; + const target = selectedElements[1]; + if (source instanceof TaskNode && target instanceof TaskNode) { + actions.push( + this.createEdgeAction(`Create Edge from ${this.getLabel(source)} to ${this.getLabel(target)}`, source, target) + ); + actions.push( + this.createWeightedEdgeAction( + `Create Weighted Edge from ${this.getLabel(source)} to ${this.getLabel(target)}`, + source, + target + ) + ); + } + } + // Delete action + + const label = selectedElementIds.length === 1 ? 'Delete' : 'Delete All'; + actions.push({ label, actions: [DeleteElementOperation.create(selectedElementIds)], icon: 'fa-minus-square' }); + + return actions; + } + + protected createEdgeActions(source: GNode, targets: GNode[]): LabeledAction[] { + const actions: LabeledAction[] = []; + targets.forEach(node => actions.push(this.createEdgeAction(`Create Edge to ${this.getLabel(node)}`, source, node))); + targets.forEach(node => + actions.push(this.createWeightedEdgeAction(`Create Weighted Edge to ${this.getLabel(node)}`, source, node)) + ); + return actions; + } + + protected createWeightedEdgeAction(label: string, source: GNode, node: GNode): LabeledAction { + return { + label, + actions: [ + CreateEdgeOperation.create({ + elementTypeId: ModelTypes.WEIGHTED_EDGE, + sourceElementId: source.id, + targetElementId: node.id + }) + ], + icon: 'fa-plus-square' + }; + } + + protected createEdgeAction(label: string, source: GNode, node: GNode): LabeledAction { + return { + label, + actions: [ + CreateEdgeOperation.create({ elementTypeId: DefaultTypes.EDGE, sourceElementId: source.id, targetElementId: node.id }) + ], + icon: 'fa-plus-square' + }; + } + + protected getLabel(node: GNode): string { + if (node instanceof TaskNode) { + return node.name; + } + return node.id; + } +} diff --git a/examples/workflow-server/src/common/provider/workflow-context-menu-item-provider.ts b/examples/workflow-server/src/common/provider/workflow-context-menu-item-provider.ts new file mode 100644 index 0000000..8d90dd8 --- /dev/null +++ b/examples/workflow-server/src/common/provider/workflow-context-menu-item-provider.ts @@ -0,0 +1,66 @@ +/******************************************************************************** + * Copyright (c) 2022-2025 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { Args, ContextMenuItemProvider, CreateNodeOperation, MenuItem, ModelState, Point, SetEditModeAction } from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; +import { GridSnapper } from '../handler/grid-snapper'; +import { ModelTypes } from '../util/model-types'; + +@injectable() +export class WorkflowContextMenuItemProvider extends ContextMenuItemProvider { + @inject(ModelState) + protected modelState: ModelState; + + getItems(selectedElementIds: string[], position: Point, args?: Args): MenuItem[] { + const editModeMenu: MenuItem = { + id: 'editMode', + label: 'Readonly Mode', + isToggled: this.modelState.isReadonly, + actions: [this.modelState.isReadonly ? SetEditModeAction.create('editable') : SetEditModeAction.create('readonly')] + }; + + if (this.modelState.isReadonly || selectedElementIds.length !== 0) { + return [editModeMenu]; + } + const snappedPosition = GridSnapper.snap(position); + const newAutTask: MenuItem = { + id: 'newAutoTask', + label: 'Automated Task', + actions: [CreateNodeOperation.create(ModelTypes.AUTOMATED_TASK, { location: snappedPosition })] + }; + const newManTask: MenuItem = { + id: 'newManualTask', + label: 'Manual Task', + actions: [CreateNodeOperation.create(ModelTypes.MANUAL_TASK, { location: snappedPosition })] + }; + + const hiddenItem: MenuItem = { + id: 'hiddenItem', + label: 'Should be hidden', + actions: [], + isVisible: false + }; + const newChildMenu: MenuItem = { + id: 'new', + label: 'New', + actions: [], + children: [newAutTask, newManTask, hiddenItem], + icon: 'add', + group: '0_new' + }; + + return [newChildMenu, editModeMenu]; + } +} diff --git a/examples/workflow-server/src/common/taskedit/edit-task-operation-handler.ts b/examples/workflow-server/src/common/taskedit/edit-task-operation-handler.ts new file mode 100644 index 0000000..f2be157 --- /dev/null +++ b/examples/workflow-server/src/common/taskedit/edit-task-operation-handler.ts @@ -0,0 +1,118 @@ +/******************************************************************************** + * Copyright (c) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { Action, Command, getOrThrow, GModelOperationHandler, hasStringProp, MaybePromise, Operation } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { TaskNode } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; +/** + * Is send from the {@link TaskEditor} to the GLSP server + * to update a feature from a specified task. + */ +export interface EditTaskOperation extends Operation { + kind: typeof EditTaskOperation.KIND; + + /** + * Id of the task that should be edited + */ + taskId: string; + + /** + * The feature that is to be updated + */ + feature: 'duration' | 'taskType'; + + /** + * The new feature value + */ + value: string; +} + +export namespace EditTaskOperation { + export const KIND = 'editTask'; + + export function is(object: any): object is EditTaskOperation { + return ( + Action.hasKind(object, KIND) && + hasStringProp(object, 'taskId') && + hasStringProp(object, 'feature') && + hasStringProp(object, 'value') + ); + } + + export function create(options: { taskId: string; feature: 'duration' | 'taskType'; value: string }): EditTaskOperation { + return { + kind: KIND, + isOperation: true, + ...options + }; + } +} + +@injectable() +export class EditTaskOperationHandler extends GModelOperationHandler { + readonly operationType = EditTaskOperation.KIND; + + createCommand(operation: EditTaskOperation): MaybePromise { + const task = getOrThrow( + this.modelState.index.findByClass(operation.taskId, TaskNode), + `Cannot find task with id '${operation.taskId}'` + ); + switch (operation.feature) { + case 'duration': { + const duration = Number.parseInt(operation.value, 10); + return duration !== task.duration // + ? this.commandOf(() => this.editDuration(task, duration)) + : undefined; + } + case 'taskType': { + return task.taskType !== operation.value // + ? this.commandOf(() => this.editTaskType(task, operation.value)) + : undefined; + } + } + } + + protected editDuration(task: TaskNode, duration: number): void { + task.duration = duration; + } + + protected editTaskType(task: TaskNode, type: string): void { + task.taskType = type; + if (type === 'manual' || type === 'automated') { + const temp = this.createTempTask(type, task); + const toAssign: Partial = { + taskType: temp.taskType, + type: temp.type, + children: temp.children, + cssClasses: temp.cssClasses + }; + Object.assign(task, toAssign); + return; + } + throw new Error(`Could not edit task '${task.id}'. Invalid type: ${type}`); + } + + protected createTempTask(type: 'automated' | 'manual', task: TaskNode): TaskNode { + return TaskNode.builder() // + .type(type === 'automated' ? ModelTypes.AUTOMATED_TASK : ModelTypes.MANUAL_TASK) + .taskType(type) + .name(task.name) + .addCssClass(type) + .children() + .build(); + } +} diff --git a/examples/workflow-server/src/common/taskedit/task-edit-context-provider.ts b/examples/workflow-server/src/common/taskedit/task-edit-context-provider.ts new file mode 100644 index 0000000..7c90290 --- /dev/null +++ b/examples/workflow-server/src/common/taskedit/task-edit-context-provider.ts @@ -0,0 +1,66 @@ +/******************************************************************************** + * Copyright (c) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { ContextActionsProvider, EditorContext, LabeledAction, MaybePromise, ModelState, toTypeGuard } from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; +import { TaskNode } from '../graph-extension'; +import { EditTaskOperation } from './edit-task-operation-handler'; + +@injectable() +export class TaskEditContextActionProvider implements ContextActionsProvider { + static readonly DURATION_PREFIX = 'duration:'; + static readonly TYPE_PREFIX = 'type:'; + static readonly TASK_PREFIX = 'task:'; + + readonly contextId = 'task-editor'; + + @inject(ModelState) + protected modelState: ModelState; + + getActions(editorContext: EditorContext): MaybePromise { + const text = editorContext.args?.['text'].toString() ?? ''; + const taskNode = this.modelState.index.findParentElement(editorContext.selectedElementIds[0], toTypeGuard(TaskNode)); + if (!taskNode) { + return []; + } + + if (text.startsWith(TaskEditContextActionProvider.TYPE_PREFIX)) { + const taskId = taskNode.id; + return [ + { label: 'type:automated', actions: [EditTaskOperation.create({ taskId, feature: 'taskType', value: 'automated' })] }, + { label: 'type:manual', actions: [EditTaskOperation.create({ taskId, feature: 'taskType', value: 'manual' })] } + ]; + } + + if (text.startsWith(TaskEditContextActionProvider.DURATION_PREFIX)) { + return []; + } + + const taskType = taskNode.type.substring(TaskEditContextActionProvider.TASK_PREFIX.length); + const duration = taskNode.duration; + return [ + { label: 'type:', actions: [], text: `${TaskEditContextActionProvider.TYPE_PREFIX}${taskType}` }, + { + label: 'duration:', + actions: [], + text: `${TaskEditContextActionProvider.DURATION_PREFIX}${duration ?? 0}` + } + ]; + } +} + +interface SetAutocompleteValueAction extends LabeledAction { + text: string; +} diff --git a/examples/workflow-server/src/common/taskedit/task-edit-validator.ts b/examples/workflow-server/src/common/taskedit/task-edit-validator.ts new file mode 100644 index 0000000..e8a29be --- /dev/null +++ b/examples/workflow-server/src/common/taskedit/task-edit-validator.ts @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (c) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { ContextEditValidator, RequestEditValidationAction, ValidationStatus } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { TaskEditContextActionProvider } from './task-edit-context-provider'; + +@injectable() +export class TaskEditValidator implements ContextEditValidator { + readonly contextId = 'task-editor'; + + validate(action: RequestEditValidationAction): ValidationStatus { + const text = action.text; + if (text.startsWith(TaskEditContextActionProvider.DURATION_PREFIX)) { + const durationString = text.substring(TaskEditContextActionProvider.DURATION_PREFIX.length); + const duration = Number.parseInt(durationString, 10); + if (Number.isNaN(duration)) { + return { severity: ValidationStatus.Severity.ERROR, message: `'${durationString}' is not a valid number.` }; + } else if (duration < 0 || duration > 100) { + return { severity: ValidationStatus.Severity.WARNING, message: `'${durationString}' should be between 0 and 100` }; + } + } else if (text.startsWith(TaskEditContextActionProvider.TYPE_PREFIX)) { + const typeString = text.substring(TaskEditContextActionProvider.TYPE_PREFIX.length); + if (typeString !== 'automated' && typeString !== 'manual') { + return { + severity: ValidationStatus.Severity.ERROR, + message: `'Type of task can only be manual or automatic. You entered '${typeString}'.` + }; + } + } + return ValidationStatus.NONE; + } +} diff --git a/examples/workflow-server/src/common/util/model-types.ts b/examples/workflow-server/src/common/util/model-types.ts new file mode 100644 index 0000000..1c84d8f --- /dev/null +++ b/examples/workflow-server/src/common/util/model-types.ts @@ -0,0 +1,54 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +export namespace ModelTypes { + export const LABEL_HEADING = 'label:heading'; + export const LABEL_TEXT = 'label:text'; + export const COMP_HEADER = 'comp:header'; + export const LABEL_ICON = 'label:icon'; + export const WEIGHTED_EDGE = 'edge:weighted'; + export const ICON = 'icon'; + export const ACTIVITY_NODE = 'activityNode'; + export const DECISION_NODE = `${ACTIVITY_NODE}:decision`; + export const MERGE_NODE = `${ACTIVITY_NODE}:merge`; + export const FORK_NODE = `${ACTIVITY_NODE}:fork`; + export const JOIN_NODE = `${ACTIVITY_NODE}:join`; + export const TASK = 'task'; + export const MANUAL_TASK = `${TASK}:manual`; + export const AUTOMATED_TASK = `${TASK}:automated`; + export const CATEGORY = 'category'; + export const STRUCTURE = 'struct'; + + export function toNodeType(type: string): string { + switch (type) { + case DECISION_NODE: + return 'decisionNode'; + case MERGE_NODE: + return 'mergeNode'; + case FORK_NODE: + return 'forkNode'; + case JOIN_NODE: + return 'joinNode'; + case MANUAL_TASK: + return 'manual'; + case AUTOMATED_TASK: + return 'automated'; + case CATEGORY: + return 'category'; + default: + return 'unknown'; + } + } +} diff --git a/examples/workflow-server/src/common/workflow-diagram-configuration.ts b/examples/workflow-server/src/common/workflow-diagram-configuration.ts new file mode 100644 index 0000000..89e0712 --- /dev/null +++ b/examples/workflow-server/src/common/workflow-diagram-configuration.ts @@ -0,0 +1,118 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { + DefaultTypes, + DiagramConfiguration, + EdgeTypeHint, + GCompartment, + GEdge, + GLabel, + GModelElementConstructor, + ServerLayoutKind, + ShapeTypeHint, + getDefaultMapping +} from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { ActivityNode, Category, TaskNode } from './graph-extension'; +import { ModelTypes as types } from './util/model-types'; + +@injectable() +export class WorkflowDiagramConfiguration implements DiagramConfiguration { + get typeMapping(): Map { + const mapping = getDefaultMapping(); + mapping.set(types.LABEL_HEADING, GLabel); + mapping.set(types.LABEL_TEXT, GLabel); + mapping.set(types.COMP_HEADER, GCompartment); + mapping.set(types.LABEL_ICON, GLabel); + mapping.set(types.WEIGHTED_EDGE, GEdge); + mapping.set(types.ICON, GCompartment); + mapping.set(types.ACTIVITY_NODE, ActivityNode); + mapping.set(types.TASK, TaskNode); + mapping.set(types.CATEGORY, Category); + mapping.set(types.STRUCTURE, GCompartment); + return mapping; + } + + get shapeTypeHints(): ShapeTypeHint[] { + return [ + createDefaultShapeTypeHint(types.MANUAL_TASK), + createDefaultShapeTypeHint(types.AUTOMATED_TASK), + createDefaultShapeTypeHint({ elementTypeId: types.FORK_NODE, resizable: false }), + createDefaultShapeTypeHint({ elementTypeId: types.JOIN_NODE, resizable: false }), + createDefaultShapeTypeHint(types.DECISION_NODE), + createDefaultShapeTypeHint(types.MERGE_NODE), + createDefaultShapeTypeHint({ + elementTypeId: types.CATEGORY, + containableElementTypeIds: [types.TASK, types.ACTIVITY_NODE, types.CATEGORY] + }) + ]; + } + + get edgeTypeHints(): EdgeTypeHint[] { + return [ + createDefaultEdgeTypeHint(DefaultTypes.EDGE), + createDefaultEdgeTypeHint({ + elementTypeId: types.WEIGHTED_EDGE, + dynamic: true, + sourceElementTypeIds: [types.ACTIVITY_NODE], + targetElementTypeIds: [types.TASK, types.ACTIVITY_NODE] + }) + ]; + } + + layoutKind = ServerLayoutKind.MANUAL; + needsClientLayout = true; + animatedUpdate = true; +} + +export function createDefaultShapeTypeHint(template: { elementTypeId: string } & Partial): ShapeTypeHint; +export function createDefaultShapeTypeHint(elementId: string): ShapeTypeHint; +export function createDefaultShapeTypeHint( + elementIdOrTemplate: string | ({ elementTypeId: string } & Partial) +): ShapeTypeHint { + const template = typeof elementIdOrTemplate === 'string' ? { elementTypeId: elementIdOrTemplate } : elementIdOrTemplate; + return { repositionable: true, deletable: true, resizable: true, reparentable: true, ...template }; +} + +export function createDefaultEdgeTypeHint(template: { elementTypeId: string } & Partial): EdgeTypeHint; +export function createDefaultEdgeTypeHint(elementId: string): EdgeTypeHint; +export function createDefaultEdgeTypeHint(elementIdOrTemplate: string | ({ elementTypeId: string } & Partial)): EdgeTypeHint { + const template = typeof elementIdOrTemplate === 'string' ? { elementTypeId: elementIdOrTemplate } : elementIdOrTemplate; + return { + repositionable: true, + deletable: true, + routable: true, + sourceElementTypeIds: [ + types.MANUAL_TASK, + types.AUTOMATED_TASK, + types.DECISION_NODE, + types.MERGE_NODE, + types.FORK_NODE, + types.JOIN_NODE, + types.CATEGORY + ], + targetElementTypeIds: [ + types.MANUAL_TASK, + types.AUTOMATED_TASK, + types.DECISION_NODE, + types.MERGE_NODE, + types.FORK_NODE, + types.JOIN_NODE, + types.CATEGORY + ], + ...template + }; +} diff --git a/examples/workflow-server/src/common/workflow-diagram-module.ts b/examples/workflow-server/src/common/workflow-diagram-module.ts new file mode 100644 index 0000000..06d5058 --- /dev/null +++ b/examples/workflow-server/src/common/workflow-diagram-module.ts @@ -0,0 +1,142 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { + BindingTarget, + CommandPaletteActionProvider, + ContextActionsProvider, + ContextEditValidator, + ContextMenuItemProvider, + DiagramConfiguration, + EdgeCreationChecker, + GLSPServerInitializer, + GModelDiagramModule, + InstanceMultiBinding, + LabelEditValidator, + ModelValidator, + MultiBinding, + NavigationTargetProvider, + NavigationTargetResolver, + OperationHandlerConstructor, + PopupModelFactory, + ServerModule, + SourceModelStorage +} from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { CreateAutomatedTaskHandler } from './handler/create-automated-task-handler'; +import { CreateCategoryHandler } from './handler/create-category-handler'; +import { CreateDecisionNodeHandler } from './handler/create-decision-node-handler'; +import { CreateEdgeHandler } from './handler/create-edge-handler'; +import { CreateForkNodeHandler } from './handler/create-fork-node-handler'; +import { CreateJoinNodeHandler } from './handler/create-join-node-handler'; +import { CreateManualTaskHandler } from './handler/create-manual-task-handler'; +import { CreateMergeNodeHandler } from './handler/create-merge-node-handler'; +import { CreateWeightedEdgeHandler } from './handler/create-weighted-edge-handler'; +import { WorkflowLabelEditValidator } from './labeledit/workflow-label-edit-validator'; +import { WorkflowModelValidator } from './marker/workflow-model-validator'; +import { WorkflowNavigationTargetResolver } from './model/workflow-navigation-target-resolver'; +import { NextNodeNavigationTargetProvider } from './provider/next-node-navigation-target-provider'; +import { NodeDocumentationNavigationTargetProvider } from './provider/node-documentation-navigation-target-provider'; +import { PreviousNodeNavigationTargetProvider } from './provider/previous-node-navigation-target-provider'; +import { WorkflowCommandPaletteActionProvider } from './provider/workflow-command-palette-action-provider'; +import { WorkflowContextMenuItemProvider } from './provider/workflow-context-menu-item-provider'; +import { EditTaskOperationHandler } from './taskedit/edit-task-operation-handler'; +import { TaskEditContextActionProvider } from './taskedit/task-edit-context-provider'; +import { TaskEditValidator } from './taskedit/task-edit-validator'; +import { WorkflowDiagramConfiguration } from './workflow-diagram-configuration'; +import { WorkflowEdgeCreationChecker } from './workflow-edge-creation-checker'; +import { CustomArgsInitContribution } from './workflow-glsp-server'; +import { WorkflowPopupFactory } from './workflow-popup-factory'; + +@injectable() +export class WorkflowServerModule extends ServerModule { + protected override configureGLSPServerInitializers(binding: MultiBinding): void { + binding.add(CustomArgsInitContribution); + } +} + +@injectable() +export class WorkflowDiagramModule extends GModelDiagramModule { + constructor(public bindSourceModelStorage: () => BindingTarget) { + super(); + } + + get diagramType(): string { + return 'workflow-diagram'; + } + + protected override configureOperationHandlers(binding: InstanceMultiBinding): void { + super.configureOperationHandlers(binding); + binding.add(CreateAutomatedTaskHandler); + binding.add(CreateManualTaskHandler); + binding.add(CreateJoinNodeHandler); + binding.add(CreateForkNodeHandler); + binding.add(CreateEdgeHandler); + binding.add(CreateWeightedEdgeHandler); + binding.add(CreateMergeNodeHandler); + binding.add(CreateDecisionNodeHandler); + binding.add(CreateCategoryHandler); + binding.add(EditTaskOperationHandler); + } + + protected bindDiagramConfiguration(): BindingTarget { + return WorkflowDiagramConfiguration; + } + + protected override bindNavigationTargetResolver(): BindingTarget | undefined { + return WorkflowNavigationTargetResolver; + } + + protected override bindContextMenuItemProvider(): BindingTarget | undefined { + return WorkflowContextMenuItemProvider; + } + + protected override bindCommandPaletteActionProvider(): BindingTarget | undefined { + return WorkflowCommandPaletteActionProvider; + } + + protected override bindLabelEditValidator(): BindingTarget | undefined { + return WorkflowLabelEditValidator; + } + + protected override bindPopupModelFactory(): BindingTarget | undefined { + return WorkflowPopupFactory; + } + + protected override bindModelValidator(): BindingTarget | undefined { + return WorkflowModelValidator; + } + + protected override configureNavigationTargetProviders(binding: MultiBinding): void { + super.configureNavigationTargetProviders(binding); + binding.add(NextNodeNavigationTargetProvider); + binding.add(PreviousNodeNavigationTargetProvider); + binding.add(NodeDocumentationNavigationTargetProvider); + } + + protected override configureContextActionProviders(binding: MultiBinding): void { + super.configureContextActionProviders(binding); + binding.add(TaskEditContextActionProvider); + } + + protected override configureContextEditValidators(binding: MultiBinding): void { + super.configureContextEditValidators(binding); + binding.add(TaskEditValidator); + } + + protected override bindEdgeCreationChecker(): BindingTarget | undefined { + return WorkflowEdgeCreationChecker; + } +} diff --git a/examples/workflow-server/src/common/workflow-edge-creation-checker.ts b/examples/workflow-server/src/common/workflow-edge-creation-checker.ts new file mode 100644 index 0000000..6addee0 --- /dev/null +++ b/examples/workflow-server/src/common/workflow-edge-creation-checker.ts @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2023-2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { EdgeCreationChecker, GModelElement } from '@eclipse-glsp/server/'; +import { injectable } from 'inversify'; +import { TaskNode } from './graph-extension'; +import { ModelTypes } from './util/model-types'; + +@injectable() +export class WorkflowEdgeCreationChecker implements EdgeCreationChecker { + isValidSource(edgeType: string, sourceElement: GModelElement): boolean { + return edgeType === ModelTypes.WEIGHTED_EDGE && sourceElement.type === ModelTypes.DECISION_NODE; + } + isValidTarget(edgeType: string, sourceElement: GModelElement, targetElement: GModelElement): boolean { + return ( + targetElement instanceof TaskNode || targetElement.type === ModelTypes.FORK_NODE || targetElement.type === ModelTypes.JOIN_NODE + ); + } +} diff --git a/examples/workflow-server/src/common/workflow-glsp-server.ts b/examples/workflow-server/src/common/workflow-glsp-server.ts new file mode 100644 index 0000000..62bf837 --- /dev/null +++ b/examples/workflow-server/src/common/workflow-glsp-server.ts @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { + ArgsUtil, + GLSPServer, + GLSPServerInitializer, + InitializeParameters, + InitializeResult, + Logger, + MaybePromise +} from '@eclipse-glsp/server'; +import { inject, injectable } from 'inversify'; + +@injectable() +export class CustomArgsInitContribution implements GLSPServerInitializer { + MESSAGE_KEY = 'message'; + TIMESTAMP_KEY = 'timestamp'; + + @inject(Logger) protected logger: Logger; + + initializeServer(server: GLSPServer, params: InitializeParameters, result: InitializeResult): MaybePromise { + if (!params.args) { + return result; + } + const timestamp = ArgsUtil.get(params.args, this.TIMESTAMP_KEY); + const message = ArgsUtil.get(params.args, this.MESSAGE_KEY); + + this.logger.info(`${timestamp}: ${message}`); + return result; + } +} diff --git a/examples/workflow-server/src/common/workflow-popup-factory.ts b/examples/workflow-server/src/common/workflow-popup-factory.ts new file mode 100644 index 0000000..ea1f494 --- /dev/null +++ b/examples/workflow-server/src/common/workflow-popup-factory.ts @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { GModelElement, GModelRoot, GPreRenderedElement, PopupModelFactory, RequestPopupModelAction } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { TaskNode } from './graph-extension'; + +const NL = '
'; + +@injectable() +export class WorkflowPopupFactory implements PopupModelFactory { + createPopupModel(element: GModelElement, action: RequestPopupModelAction): GModelRoot | undefined { + if (element && element instanceof TaskNode) { + const popupTitle = GPreRenderedElement.builder() + .id('popup-title') + .code(`
${element.name}
`) + .build(); + + const popupBody = GPreRenderedElement.builder() + .id('popup-body') + .code(`
${this.generateBody(element)}
`) + .build(); + + return GModelRoot.builder().type('html').canvasBounds(action.bounds).id('sprotty-popup').add(popupTitle).add(popupBody).build(); + } + return undefined; + } + + protected generateBody(task: TaskNode): string { + return `Type: ${task.taskType} ${NL} + Duration: ${task.duration} ${NL} + Reference: ${task.references} ${NL}`; + } +} diff --git a/examples/workflow-server/src/node/.indexignore b/examples/workflow-server/src/node/.indexignore new file mode 100644 index 0000000..c998fd9 --- /dev/null +++ b/examples/workflow-server/src/node/.indexignore @@ -0,0 +1,4 @@ +# The app.ts is the entrypoint when the workflow server is used bundled in an external node process +# It launches the server when imported so it has to be excluded from the default export to avoid unintended +# autostarts when the server package is directly integrated into a node backed (e.g. Theia) +app.ts \ No newline at end of file diff --git a/examples/workflow-server/src/node/app.ts b/examples/workflow-server/src/node/app.ts new file mode 100644 index 0000000..2033998 --- /dev/null +++ b/examples/workflow-server/src/node/app.ts @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import 'reflect-metadata'; + +import { ElkLayoutModule } from '@eclipse-glsp/layout-elk'; +import { GModelStorage, Logger, SocketServerLauncher, WebSocketServerLauncher, createAppModule } from '@eclipse-glsp/server/node'; +import { Container } from 'inversify'; + +import { WorkflowLayoutConfigurator } from '../common/layout/workflow-layout-configurator'; +import { WorkflowMcpDiagramModule } from '../common/mcp/workflow-mcp-diagram-module'; +import { WorkflowDiagramModule, WorkflowServerModule } from '../common/workflow-diagram-module'; +import { WorkflowMcpServerModule } from './mcp/workflow-mcp-module'; +import { createWorkflowCliParser } from './workflow-cli-parser'; + +async function launch(argv?: string[]): Promise { + const options = createWorkflowCliParser().parse(argv); + const appContainer = new Container(); + appContainer.load(createAppModule(options)); + const logger = appContainer.get(Logger); + + // Add fallback hooks to catch unhandled exceptions & promise rejections and prevent the node process from crashing + process.on('unhandledRejection', (reason, p) => { + logger.error('Unhandled Rejection:', p, reason); + }); + + process.on('uncaughtException', error => { + logger.error('Uncaught exception:', error); + }); + + const serverModule = new WorkflowServerModule().configureDiagramModule( + new WorkflowDiagramModule(() => GModelStorage), + new ElkLayoutModule({ algorithms: ['layered'], layoutConfigurator: WorkflowLayoutConfigurator }), + new WorkflowMcpDiagramModule() + ); + const mcpServerModule = new WorkflowMcpServerModule(); + if (options.webSocket) { + const launcher = appContainer.resolve(WebSocketServerLauncher); + launcher.configure(serverModule, mcpServerModule); + await launcher.start({ port: options.port, host: options.host, path: 'workflow' }); + } else { + const launcher = appContainer.resolve(SocketServerLauncher); + launcher.configure(serverModule, mcpServerModule); + await launcher.start({ port: options.port, host: options.host }); + } +} + +launch(process.argv).catch(error => console.error('Error in workflow server launcher:', error)); diff --git a/examples/workflow-server/src/node/index.ts b/examples/workflow-server/src/node/index.ts new file mode 100644 index 0000000..8df6a5b --- /dev/null +++ b/examples/workflow-server/src/node/index.ts @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (c) 2022-2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +export * from './mcp/workflow-mcp-module'; +export * from './reexport'; +export * from './workflow-cli-parser'; diff --git a/examples/workflow-server/src/node/mcp/workflow-mcp-module.ts b/examples/workflow-server/src/node/mcp/workflow-mcp-module.ts new file mode 100644 index 0000000..7566952 --- /dev/null +++ b/examples/workflow-server/src/node/mcp/workflow-mcp-module.ts @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { NodeMcpServerModule } from '@eclipse-glsp/server-mcp/node'; + +/** + * Workflow-scope MCP module — adopter hook for future workflow-specific server-scope bindings. + * Currently a no-op subclass; diagram-scope customizations live on `WorkflowMcpDiagramModule`. + */ +export class WorkflowMcpServerModule extends NodeMcpServerModule {} diff --git a/examples/workflow-server/src/node/reexport.ts b/examples/workflow-server/src/node/reexport.ts new file mode 100644 index 0000000..61a2c52 --- /dev/null +++ b/examples/workflow-server/src/node/reexport.ts @@ -0,0 +1,16 @@ +/******************************************************************************** + * Copyright (c) 2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +export * from '../common'; diff --git a/examples/workflow-server/src/node/workflow-cli-parser.ts b/examples/workflow-server/src/node/workflow-cli-parser.ts new file mode 100644 index 0000000..09c62b6 --- /dev/null +++ b/examples/workflow-server/src/node/workflow-cli-parser.ts @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { CliParser, createSocketCliParser, defaultSocketLaunchOptions, SocketLaunchOptions } from '@eclipse-glsp/server/node'; + +export interface WorkflowLaunchOptions extends SocketLaunchOptions { + webSocket: boolean; +} + +export function createWorkflowCliParser( + defaultOptions: WorkflowLaunchOptions = { webSocket: false, ...defaultSocketLaunchOptions } +): CliParser { + const parser = createSocketCliParser(defaultOptions); + parser.command.option('-w , --webSocket', 'Flag to use websocket launcher instead of default launcher', false); + return parser; +} diff --git a/examples/workflow-server/tsconfig.json b/examples/workflow-server/tsconfig.json new file mode 100644 index 0000000..a82e1ff --- /dev/null +++ b/examples/workflow-server/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@eclipse-glsp/ts-config/", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "composite": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["src", "src/**/*.json"], + "references": [ + { + "path": "../../packages/server" + }, + { + "path": "../../packages/server-mcp" + }, + { + "path": "../../packages/layout-elk" + } + ] +} diff --git a/examples/workflow-standalone/LICENSE b/examples/workflow-standalone/LICENSE new file mode 100644 index 0000000..e079b08 --- /dev/null +++ b/examples/workflow-standalone/LICENSE @@ -0,0 +1,642 @@ +This program and the accompanying materials are made available under the +terms of the Eclipse Public License v. 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0, or GNU General Public License, version 2 +with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. + +# Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + 1. DEFINITIONS + + "Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + + "Contributor" means any person or entity that Distributes the Program. + + "Licensed Patents" mean patent claims licensable by a Contributor which + are necessarily infringed by the use or sale of its Contribution alone + or when combined with the Program. + + "Program" means the Contributions Distributed in accordance with this + Agreement. + + "Recipient" means anyone who receives the Program under this Agreement + or any Secondary License (as applicable), including Contributors. + + "Derivative Works" shall mean any work, whether in Source Code or other + form, that is based on (or derived from) the Program and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. + + "Modified Works" shall mean any work in Source Code or other form that + results from an addition to, deletion from, or modification of the + contents of the Program, including, for purposes of clarity any new file + in Source Code form that contains any contents of the Program. Modified + Works shall not include works that contain only declarations, + interfaces, types, classes, structures, or files of the Program solely + in each case in order to link to, bind by name, or subclass the Program + or Modified Works thereof. + + "Distribute" means the acts of a) distributing or b) making available + in any manner that enables the transfer of a copy. + + "Source Code" means the form of a Program preferred for making + modifications, including but not limited to software source code, + documentation source, and configuration files. + + "Secondary License" means either the GNU General Public License, + Version 2.0, or any later versions of that license, including any + exceptions or additional permissions as identified by the initial + Contributor. + + 2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + + 3. REQUIREMENTS + + 3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + + 3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + + 3.3 Contributors may not remove or alter any copyright, patent, + trademark, attribution notices, disclaimers of warranty, or limitations + of liability ("notices") contained within the Program from any copy of + the Program which they Distribute, provided that Contributors may add + their own appropriate notices. + + 4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities + with respect to end users, business partners and the like. While this + license is intended to facilitate the commercial use of the Program, + the Contributor who includes the Program in a commercial product + offering should do so in a manner which does not create potential + liability for other Contributors. Therefore, if a Contributor includes + the Program in a commercial product offering, such Contributor + ("Commercial Contributor") hereby agrees to defend and indemnify every + other Contributor ("Indemnified Contributor") against any losses, + damages and costs (collectively "Losses") arising from claims, lawsuits + and other legal actions brought by a third party against the Indemnified + Contributor to the extent caused by the acts or omissions of such + Commercial Contributor in connection with its distribution of the Program + in a commercial product offering. The obligations in this section do not + apply to any claims or Losses relating to any actual or alleged + intellectual property infringement. In order to qualify, an Indemnified + Contributor must: a) promptly notify the Commercial Contributor in + writing of such claim, and b) allow the Commercial Contributor to control, + and cooperate with the Commercial Contributor in, the defense and any + related settlement negotiations. The Indemnified Contributor may + participate in any such claim at its own expense. + + For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance + claims, or offers warranties related to Product X, those performance + claims and warranties are such Commercial Contributor's responsibility + alone. Under this section, the Commercial Contributor would have to + defend claims against the other Contributors related to those performance + claims and warranties, and if a court requires any other Contributor to + pay any damages as a result, the Commercial Contributor must pay + those damages. + + 5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" + BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR + IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF + TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR + PURPOSE. Each Recipient is solely responsible for determining the + appropriateness of using and distributing the Program and assumes all + risks associated with its exercise of rights under this Agreement, + including but not limited to the risks and costs of program errors, + compliance with applicable laws, damage to or loss of data, programs + or equipment, and unavailability or interruption of operations. + + 6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS + SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST + PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE + EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + + 7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further + action by the parties hereto, such provision shall be reformed to the + minimum extent necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity + (including a cross-claim or counterclaim in a lawsuit) alleging that the + Program itself (excluding combinations of the Program with other software + or hardware) infringes such Recipient's patent(s), then such Recipient's + rights granted under Section 2(b) shall terminate as of the date such + litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it + fails to comply with any of the material terms or conditions of this + Agreement and does not cure such failure in a reasonable period of + time after becoming aware of such noncompliance. If all Recipient's + rights under this Agreement terminate, Recipient agrees to cease use + and distribution of the Program as soon as reasonably practicable. + However, Recipient's obligations under this Agreement and any licenses + granted by Recipient relating to the Program shall continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, + but in order to avoid inconsistency the Agreement is copyrighted and + may only be modified in the following manner. The Agreement Steward + reserves the right to publish new versions (including revisions) of + this Agreement from time to time. No one other than the Agreement + Steward has the right to modify this Agreement. The Eclipse Foundation + is the initial Agreement Steward. The Eclipse Foundation may assign the + responsibility to serve as the Agreement Steward to a suitable separate + entity. Each new version of the Agreement will be given a distinguishing + version number. The Program (including Contributions) may always be + Distributed subject to the version of the Agreement under which it was + received. In addition, after a new version of the Agreement is published, + Contributor may elect to Distribute the Program (including its + Contributions) under the new version. + + Except as expressly stated in Sections 2(a) and 2(b) above, Recipient + receives no rights or licenses to the intellectual property of any + Contributor under this Agreement, whether expressly, by implication, + estoppel or otherwise. All rights in the Program not expressly granted + under this Agreement are reserved. Nothing in this Agreement is intended + to be enforceable by any entity that is not a Contributor or Recipient. + No third-party beneficiary rights are created under this Agreement. + + Exhibit A - Form of Secondary Licenses Notice + + "This Source Code may also be made available under the following + Secondary Licenses when the conditions for such availability set forth + in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), + version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. + +--- + +## The GNU General Public License (GPL) Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor + Boston, MA 02110-1335 + USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your freedom to + share and change it. By contrast, the GNU General Public License is + intended to guarantee your freedom to share and change free software--to + make sure the software is free for all its users. This General Public + License applies to most of the Free Software Foundation's software and + to any other program whose authors commit to using it. (Some other Free + Software Foundation software is covered by the GNU Library General + Public License instead.) You can apply it to your programs, too. + + When we speak of free software, we are referring to freedom, not price. + Our General Public Licenses are designed to make sure that you have the + freedom to distribute copies of free software (and charge for this + service if you wish), that you receive source code or can get it if you + want it, that you can change the software or use pieces of it in new + free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid anyone + to deny you these rights or to ask you to surrender the rights. These + restrictions translate to certain responsibilities for you if you + distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether gratis + or for a fee, you must give the recipients all the rights that you have. + You must make sure that they, too, receive or can get the source code. + And you must show them these terms so they know their rights. + + We protect your rights with two steps: (1) copyright the software, and + (2) offer you this license which gives you legal permission to copy, + distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain + that everyone understands that there is no warranty for this free + software. If the software is modified by someone else and passed on, we + want its recipients to know that what they have is not the original, so + that any problems introduced by others will not reflect on the original + authors' reputations. + + Finally, any free program is threatened constantly by software patents. + We wish to avoid the danger that redistributors of a free program will + individually obtain patent licenses, in effect making the program + proprietary. To prevent this, we have made it clear that any patent must + be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and + modification follow. + + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains a + notice placed by the copyright holder saying it may be distributed under + the terms of this General Public License. The "Program", below, refers + to any such program or work, and a "work based on the Program" means + either the Program or any derivative work under copyright law: that is + to say, a work containing the Program or a portion of it, either + verbatim or with modifications and/or translated into another language. + (Hereinafter, translation is included without limitation in the term + "modification".) Each licensee is addressed as "you". + + Activities other than copying, distribution and modification are not + covered by this License; they are outside its scope. The act of running + the Program is not restricted, and the output from the Program is + covered only if its contents constitute a work based on the Program + (independent of having been made by running the Program). Whether that + is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's source + code as you receive it, in any medium, provided that you conspicuously + and appropriately publish on each copy an appropriate copyright notice + and disclaimer of warranty; keep intact all the notices that refer to + this License and to the absence of any warranty; and give any other + recipients of the Program a copy of this License along with the Program. + + You may charge a fee for the physical act of transferring a copy, and + you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion of + it, thus forming a work based on the Program, and copy and distribute + such modifications or work under the terms of Section 1 above, provided + that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any part + thereof, to be licensed as a whole at no charge to all third parties + under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a notice + that there is no warranty (or else, saying that you provide a + warranty) and that users may redistribute the program under these + conditions, and telling the user how to view a copy of this License. + (Exception: if the Program itself is interactive but does not + normally print such an announcement, your work based on the Program + is not required to print an announcement.) + + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Program, and + can be reasonably considered independent and separate works in + themselves, then this License, and its terms, do not apply to those + sections when you distribute them as separate works. But when you + distribute the same sections as part of a whole which is a work based on + the Program, the distribution of the whole must be on the terms of this + License, whose permissions for other licensees extend to the entire + whole, and thus to each and every part regardless of who wrote it. + + Thus, it is not the intent of this section to claim rights or contest + your rights to work written entirely by you; rather, the intent is to + exercise the right to control the distribution of derivative or + collective works based on the Program. + + In addition, mere aggregation of another work not based on the Program + with the Program (or with a work based on the Program) on a volume of a + storage or distribution medium does not bring the other work under the + scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, + under Section 2) in object code or executable form under the terms of + Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your cost + of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed + only for noncommercial distribution and only if you received the + program in object code or executable form with such an offer, in + accord with Subsection b above.) + + The source code for a work means the preferred form of the work for + making modifications to it. For an executable work, complete source code + means all the source code for all modules it contains, plus any + associated interface definition files, plus the scripts used to control + compilation and installation of the executable. However, as a special + exception, the source code distributed need not include anything that is + normally distributed (in either source or binary form) with the major + components (compiler, kernel, and so on) of the operating system on + which the executable runs, unless that component itself accompanies the + executable. + + If distribution of executable or object code is made by offering access + to copy from a designated place, then offering equivalent access to copy + the source code from the same place counts as distribution of the source + code, even though third parties are not compelled to copy the source + along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program + except as expressly provided under this License. Any attempt otherwise + to copy, modify, sublicense or distribute the Program is void, and will + automatically terminate your rights under this License. However, parties + who have received copies, or rights, from you under this License will + not have their licenses terminated so long as such parties remain in + full compliance. + + 5. You are not required to accept this License, since you have not + signed it. However, nothing else grants you permission to modify or + distribute the Program or its derivative works. These actions are + prohibited by law if you do not accept this License. Therefore, by + modifying or distributing the Program (or any work based on the + Program), you indicate your acceptance of this License to do so, and all + its terms and conditions for copying, distributing or modifying the + Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further restrictions + on the recipients' exercise of the rights granted herein. You are not + responsible for enforcing compliance by third parties to this License. + + 7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot distribute + so as to satisfy simultaneously your obligations under this License and + any other pertinent obligations, then as a consequence you may not + distribute the Program at all. For example, if a patent license would + not permit royalty-free redistribution of the Program by all those who + receive copies directly or indirectly through you, then the only way you + could satisfy both it and this License would be to refrain entirely from + distribution of the Program. + + If any portion of this section is held invalid or unenforceable under + any particular circumstance, the balance of the section is intended to + apply and the section as a whole is intended to apply in other + circumstances. + + It is not the purpose of this section to induce you to infringe any + patents or other property right claims or to contest validity of any + such claims; this section has the sole purpose of protecting the + integrity of the free software distribution system, which is implemented + by public license practices. Many people have made generous + contributions to the wide range of software distributed through that + system in reliance on consistent application of that system; it is up to + the author/donor to decide if he or she is willing to distribute + software through any other system and a licensee cannot impose that choice. + + This section is intended to make thoroughly clear what is believed to be + a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in + certain countries either by patents or by copyrighted interfaces, the + original copyright holder who places the Program under this License may + add an explicit geographical distribution limitation excluding those + countries, so that distribution is permitted only in or among countries + not thus excluded. In such case, this License incorporates the + limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new + versions of the General Public License from time to time. Such new + versions will be similar in spirit to the present version, but may + differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the Program + specifies a version number of this License which applies to it and "any + later version", you have the option of following the terms and + conditions either of that version or of any later version published by + the Free Software Foundation. If the Program does not specify a version + number of this License, you may choose any version ever published by the + Free Software Foundation. + + 10. If you wish to incorporate parts of the Program into other free + programs whose distribution conditions are different, write to the + author to ask for permission. For software which is copyrighted by the + Free Software Foundation, write to the Free Software Foundation; we + sometimes make exceptions for this. Our decision will be guided by the + two goals of preserving the free status of all derivatives of our free + software and of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO + WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. + EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR + OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, + EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE + ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH + YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL + NECESSARY SERVICING, REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN + WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY + AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR + DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL + DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM + (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED + INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF + THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR + OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest to + attach them to the start of each source file to most effectively convey + the exclusion of warranty; and each file should have at least the + "copyright" line and a pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA + + Also add information on how to contact you by electronic and paper mail. + + If the program is interactive, make it output a short notice like this + when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type + `show w'. This is free software, and you are welcome to redistribute + it under certain conditions; type `show c' for details. + + The hypothetical commands `show w' and `show c' should show the + appropriate parts of the General Public License. Of course, the commands + you use may be called something other than `show w' and `show c'; they + could even be mouse-clicks or menu items--whatever suits your program. + + You should also get your employer (if you work as a programmer) or your + school, if any, to sign a "copyright disclaimer" for the program, if + necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program `Gnomovision' (which makes passes at compilers) written by + James Hacker. + + signature of Ty Coon, 1 April 1989 + Ty Coon, President of Vice + + This General Public License does not permit incorporating your program + into proprietary programs. If your program is a subroutine library, you + may consider it more useful to permit linking proprietary applications + with the library. If this is what you want to do, use the GNU Library + General Public License instead of this License. + +--- + +## CLASSPATH EXCEPTION + + Linking this library statically or dynamically with other modules is + making a combined work based on this library. Thus, the terms and + conditions of the GNU General Public License version 2 cover the whole + combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent + modules, and to copy and distribute the resulting executable under + terms of your choice, provided that you also meet, for each linked + independent module, the terms and conditions of the license of that + module. An independent module is a module which is not derived from or + based on this library. If you modify this library, you may extend this + exception to your version of the library, but you are not obligated to + do so. If you do not wish to do so, delete this exception statement + from your version. diff --git a/examples/workflow-standalone/README.md b/examples/workflow-standalone/README.md new file mode 100644 index 0000000..d009722 --- /dev/null +++ b/examples/workflow-standalone/README.md @@ -0,0 +1,121 @@ +# Workflow Standalone Example + +Standalone browser application for the GLSP Workflow example diagram. +This package supports two modes: a **Node** mode that connects to an external GLSP server via WebSocket (Node.js or Java), and a **Browser** mode that runs the GLSP server entirely in-browser as a Web Worker. + +## Prerequisites + +Build the GLSP client packages from the repository root: + +```bash +pnpm build +``` + +## Node Mode (WebSocket) + +In this mode the client connects to an external GLSP server over a WebSocket. By default a pre-built Node.js server is downloaded and started, but this mode can also be used with a [Java-based GLSP server](https://github.com/eclipse-glsp/glsp-server#workflow-diagram-example). + +```bash +pnpm start +``` + +This downloads the GLSP server (on first run), starts it, and launches the webpack dev server on port **8082**. +The application opens at `http://localhost:8082/diagram.html`. + +To use a locally built server bundle: + +```bash +pnpm start --external-server /path/to/wf-glsp-server-node.js +``` + +This copies the provided bundle, skips the npm download, and starts the server from it. + +To use your own GLSP server running from source (e.g. launched from your IDE), start the client without any built-in server: + +```bash +pnpm start --external-server +``` + +You can also configure the server port and host: + +```bash +pnpm start --port 9090 --host 0.0.0.0 +``` + +## Browser Mode (Web Worker) + +In this mode the GLSP server is bundled as a Web Worker and runs directly in the browser. No external server process is needed. + +```bash +pnpm start:browser +``` + +This downloads the Web Worker server bundle (on first run) and launches the webpack dev server on port **8083**. +The application opens at `http://localhost:8083/diagram.html`. + +To use a locally built Web Worker server bundle instead of the published one: + +```bash +pnpm start:browser --external-server /path/to/wf-glsp-server-web.js +``` + +This copies the provided bundle into the `server/` directory and skips the npm download. + +## Development (Watch Mode) + +For active development, the `dev` scripts compile TypeScript in watch mode and start the webpack dev server with hot reloading: + +```bash +# Node mode – watches sources, starts GLSP server, starts webpack dev server +pnpm dev + +# Browser mode – watches sources, starts webpack dev server +pnpm dev:browser +``` + +Changes to TypeScript sources are recompiled automatically. Reload the browser to pick up changes. + +## Building + +```bash +# Node bundle (default) +pnpm build + +# Browser bundle +pnpm build:browser +``` + +Both produce a `bundle.js` in the `app/` directory. The browser build additionally downloads the Web Worker server bundle and copies it into the app directory. + +## Additional Options + +All `start` and `dev` scripts support the following flags: + +- `--external-server [path]` – Use an external server instead of the default bundled one. + With a **path**: copies the provided bundle and skips the npm download. In node mode the server is started from the copied bundle; in browser mode it is served as a Web Worker. + **Without a path** (Node mode only): skips the server download and startup entirely — you run the server yourself. +- `--no-open` – Don't open the browser automatically +- `--port ` – Set the GLSP server port (Node mode only, default: 8081) +- `--host ` – Set the GLSP server host (Node mode only, default: localhost) +- `--client-port ` – Set the webpack dev server port (default: 8082 in Node mode, 8083 in Browser mode) + +The server bundle download can also be skipped by setting the `SKIP_DOWNLOAD=true` environment variable. + +## URL Parameters + +The running application reads the following query parameters from the diagram URL. They are independent and can be combined, e.g.: + +``` +http://localhost:8082/diagram.html?grid&theme=ember&mode=dark +``` + +| Parameter | Values | Default | Description | +| ---------- | ------------------------------------------------ | ------------- | ---------------------------------------------------------------------------------------------- | +| `readonly` | _flag_ (presence only) | editable | Open the diagram in read-only mode (editing disabled). | +| `grid` | _flag_ (presence only) | off | Show the background grid. | +| `theme` | `tide`, `graphite`, `ember`, `orchid`, `verdant` | `tide` | Set the color theme. Persisted in local storage, so it also updates the in-app theme switcher. | +| `mode` | `light`, `dark` | OS preference | Set light or dark appearance. Persisted in local storage. | +| `mcp` | _flag_ (presence only) | off | Enable the MCP server integration (Node mode only). Also available via the `*:mcp` scripts. | + +Flag parameters only need to be present (their value is ignored), e.g. `?grid` or `?readonly`. +Invalid `theme`/`mode` values are ignored and fall back to the stored value or, for `mode`, the OS preference. diff --git a/examples/workflow-standalone/app/diagram.html b/examples/workflow-standalone/app/diagram.html new file mode 100644 index 0000000..cf26f1e --- /dev/null +++ b/examples/workflow-standalone/app/diagram.html @@ -0,0 +1,488 @@ + + + + + + GLSP Workflow Editor + + + + + + + + + + + + +
+
+
+ +
+ Workflow Editor + Demo +
+ +
+ + + +
+ + +
+ + +
+ +
+ +
+ +
+
+
+ +
+
+ + Keyboard Shortcuts +
+
+
+
+ Navigate + Scroll Zoom in / out + Middle Drag Pan canvas + Arrow keys Move viewport / element + Ctrl Shift F Fit to screen + Ctrl Shift C Center selected + N Global navigation mode + Alt N Local navigation mode +
+
+ Zoom + + - Zoom viewport / element + Ctrl 0 Reset viewport + Ctrl + Zoom in via grid +
+
+ Edit + Ctrl Z Undo + Ctrl Y Redo + Del Delete + Ctrl A Select all + Esc Deselect all + Ctrl S Save + Ctrl Shift E Export SVG +
+
+ Resize (selected) + Alt A Enter resize mode + + - Grow / shrink + Ctrl 0 Reset size + Esc Exit resize mode +
+
+ Tools & Accessibility + Alt P Focus palette + Alt G Focus diagram + Alt H Show shortcut help + Ctrl Space Command palette + Ctrl F Search elements + Ctrl . Ctrl , Next / prev marker + 1 Select tool + 2 Eraser + 3 Marquee + 4 Validate + 5 Search +
+
+
+
+
+
+ + + + + diff --git a/examples/workflow-standalone/app/example1.wf b/examples/workflow-standalone/app/example1.wf new file mode 100644 index 0000000..5500e81 --- /dev/null +++ b/examples/workflow-standalone/app/example1.wf @@ -0,0 +1,814 @@ +{ + "cssClasses": [], + "type": "graph", + "position": { + "x": 0, + "y": 0 + }, + "id": "sprotty", + "revision": 1, + "children": [ + { + "cssClasses": [ + "task", + "automated" + ], + "type": "task:automated", + "id": "task_ChkTp", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 220, + "y": 150 + }, + "name": "ChkTp", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 82.4482421875, + "prefHeight": 30 + }, + "size": { + "width": 82.4482421875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_ChkTp_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 20.6171875, + "y": 13 + }, + "id": "task_ChkTp_label", + "text": "ChkTp", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 41.4482421875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": [ + "task", + "manual" + ], + "type": "task:manual", + "id": "task_Push", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 70, + "y": 100 + }, + "name": "Push", + "taskType": "manual", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 72.921875, + "prefHeight": 30 + }, + "size": { + "width": 72.921875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_Push_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 15.9609375, + "y": 13 + }, + "id": "task_Push_label", + "text": "Push", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 31.921875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": [ + "forkOrJoin" + ], + "type": "activityNode:fork", + "id": "fork_1", + "position": { + "x": 170, + "y": 90 + }, + "nodeType": "forkNode", + "size": { + "width": 10, + "height": 50 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_Push_fork_1", + "sourceId": "task_Push", + "targetId": "fork_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_fork1_task_ChkTp", + "sourceId": "fork_1", + "targetId": "task_ChkTp", + "children": [] + }, + { + "cssClasses": [ + "task", + "automated" + ], + "type": "task:automated", + "id": "task_ChkWt", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 220, + "y": 50 + }, + "name": "ChkWt", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 83.1103515625, + "prefHeight": 30 + }, + "size": { + "width": 83.1103515625, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_ChkWt_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 21, + "y": 13 + }, + "id": "task_ChkWt_label", + "text": "ChkWt", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 42.1103515625, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": [ + "decision" + ], + "type": "activityNode:decision", + "id": "decision_1", + "position": { + "x": 330, + "y": 50 + }, + "nodeType": "decisionNode", + "size": { + "width": 32, + "height": 32 + }, + "resizeLocations": [ + "top", + "right", + "bottom", + "left" + ], + "children": [] + }, + { + "cssClasses": [ + "decision" + ], + "type": "activityNode:decision", + "id": "decision2", + "position": { + "x": 330, + "y": 150 + }, + "nodeType": "decisionNode", + "size": { + "width": 32, + "height": 32 + }, + "resizeLocations": [ + "top", + "right", + "bottom", + "left" + ], + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_ChkWt_decision1", + "sourceId": "task_ChkWt", + "targetId": "decision_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_fork1_task_ChkWt", + "sourceId": "fork_1", + "targetId": "task_ChkWt", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_ChkTp_decision2", + "sourceId": "task_ChkTp", + "targetId": "decision2", + "children": [] + }, + { + "cssClasses": [ + "task", + "automated" + ], + "type": "task:automated", + "id": "task_PreHeat", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 390, + "y": 180 + }, + "name": "PreHeat", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 92.46875, + "prefHeight": 30 + }, + "size": { + "width": 92.46875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_PreHeat_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 25.6796875, + "y": 13 + }, + "id": "task_PreHeat_label", + "text": "PreHeat", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 51.46875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": [ + "task", + "automated" + ], + "type": "task:automated", + "id": "task_KeepTp", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 390, + "y": 130 + }, + "name": "KeepTp", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 90.248046875, + "prefHeight": 30 + }, + "size": { + "width": 90.248046875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_KeepTp_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 24.5234375, + "y": 13 + }, + "id": "task_KeepTp_label", + "text": "KeepTp", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 49.248046875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": [ + "task", + "manual" + ], + "type": "task:manual", + "id": "task_RflWt", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 390, + "y": 20 + }, + "name": "RflWt", + "taskType": "manual", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 75.32421875, + "prefHeight": 30 + }, + "size": { + "width": 75.32421875, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_RflWt_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 17.109375, + "y": 13 + }, + "id": "task_RflWt_label", + "text": "RflWt", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 34.32421875, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": [ + "task", + "automated" + ], + "type": "task:automated", + "id": "task_WtOK", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 390, + "y": 80 + }, + "name": "WtOK", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 78.9931640625, + "prefHeight": 30 + }, + "size": { + "width": 78.9931640625, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_WtOK_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 18.671875, + "y": 13 + }, + "id": "task_WtOK_label", + "text": "WtOK", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 37.9931640625, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": [ + "medium" + ], + "type": "edge:weighted", + "routingPoints": [], + "id": "edge_decision1_task_RflWt", + "sourceId": "decision_1", + "targetId": "task_RflWt", + "probability": "medium", + "children": [] + }, + { + "cssClasses": [ + "medium" + ], + "type": "edge:weighted", + "routingPoints": [], + "id": "edge_decision_1_task_WtOK", + "sourceId": "decision_1", + "targetId": "task_WtOK", + "probability": "medium", + "children": [] + }, + { + "cssClasses": [ + "medium" + ], + "type": "edge:weighted", + "routingPoints": [], + "id": "edege_decision_2_task_KeepTp", + "sourceId": "decision2", + "targetId": "task_KeepTp", + "probability": "medium", + "children": [] + }, + { + "cssClasses": [ + "medium" + ], + "type": "edge:weighted", + "routingPoints": [], + "id": "edege_decision2_task_PreHeat", + "sourceId": "decision2", + "targetId": "task_PreHeat", + "probability": "medium", + "children": [] + }, + { + "cssClasses": [ + "forkOrJoin" + ], + "type": "activityNode:join", + "id": "join_1", + "position": { + "x": 560, + "y": 90 + }, + "nodeType": "joinNode", + "size": { + "width": 10, + "height": 50 + }, + "children": [] + }, + { + "cssClasses": [ + "task", + "automated" + ], + "type": "task:automated", + "id": "task_Brew", + "layout": "hbox", + "args": { + "radiusTopLeft": 5, + "radiusBottomLeft": 5, + "radiusBottomRight": 5, + "radiusTopRight": 5 + }, + "position": { + "x": 600, + "y": 100 + }, + "name": "Brew", + "taskType": "automated", + "layoutOptions": { + "paddingRight": 10, + "prefWidth": 73.20530319213867, + "prefHeight": 30 + }, + "size": { + "width": 73.20530319213867, + "height": 30 + }, + "children": [ + { + "cssClasses": [], + "type": "icon", + "id": "task_Brew_icon", + "position": { + "x": 5, + "y": 5 + }, + "size": { + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "cssClasses": [], + "type": "label:heading", + "alignment": { + "x": 15.953125, + "y": 13 + }, + "id": "task_Brew_label", + "text": "Brew", + "position": { + "x": 31, + "y": 7 + }, + "size": { + "width": 31.90625, + "height": 16 + }, + "children": [] + } + ] + }, + { + "cssClasses": [ + "merge" + ], + "type": "activityNode:merge", + "id": "merge_1", + "position": { + "x": 500, + "y": 50 + }, + "nodeType": "mergeNode", + "size": { + "width": 32, + "height": 32 + }, + "resizeLocations": [ + "top", + "right", + "bottom", + "left" + ], + "children": [] + }, + { + "cssClasses": [ + "merge" + ], + "type": "activityNode:merge", + "id": "merge_2", + "position": { + "x": 500, + "y": 150 + }, + "nodeType": "mergeNode", + "size": { + "width": 32, + "height": 32 + }, + "resizeLocations": [ + "top", + "right", + "bottom", + "left" + ], + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_RflWt_merge_1", + "sourceId": "task_RflWt", + "targetId": "merge_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_WTOK_merge_1", + "sourceId": "task_WtOK", + "targetId": "merge_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_task_KeepTp_merge_2", + "sourceId": "task_KeepTp", + "targetId": "merge_2", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edege_task_PreHeat_merge_2", + "sourceId": "task_PreHeat", + "targetId": "merge_2", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_merge_2_join_1", + "sourceId": "merge_2", + "targetId": "join_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_merge_1_join_1", + "sourceId": "merge_1", + "targetId": "join_1", + "children": [] + }, + { + "cssClasses": [], + "type": "edge", + "routingPoints": [], + "id": "edge_join_1_task_Brew", + "sourceId": "join_1", + "targetId": "task_Brew", + "children": [] + } + ] +} \ No newline at end of file diff --git a/examples/workflow-standalone/css/app.css b/examples/workflow-standalone/css/app.css new file mode 100644 index 0000000..c83739a --- /dev/null +++ b/examples/workflow-standalone/css/app.css @@ -0,0 +1,633 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* + * Application frame: shell, card, title bar, theme switcher, canvas, footer. + * Single CSS entry point — imports the theme layer and the diagram styling. + */ + +@import './themes.css'; +@import './diagram.css'; + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* + * Respect the user's reduced-motion preference for the diagram surface. Besides being an + * accessibility nicety, this keeps the canvas and its elements geometrically stable immediately + * after rendering/interaction -> improves E2E testing reliability + */ +@media (prefers-reduced-motion: reduce) { + .app-card { + animation: none !important; + } + + .sprotty-graph *, + .sprotty-resize-handle, + .sprotty-routing-handle { + transition: none !important; + } +} + +html, +body { + width: 100%; + height: 100%; + overflow: hidden; + font-family: var(--font-body); + color: var(--text-1); + background: var(--app-bg); + -webkit-font-smoothing: antialiased; + transition: + background 0.4s ease, + color 0.4s ease; +} + +/* --- App Shell + Atmosphere --- */ + +.app-shell { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + padding: 14px; +} + +.app-shell::before { + content: ''; + position: absolute; + inset: 0; + background: var(--atmosphere); + transition: background 0.4s ease; + pointer-events: none; +} + +.app-card { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + max-width: 1600px; + max-height: 920px; + border-radius: 14px; + overflow: hidden; + background: var(--card-bg); + border: 1px solid var(--card-border); + box-shadow: var(--card-shadow); + transition: + background 0.4s ease, + border-color 0.4s ease, + box-shadow 0.4s ease; + /* `backwards` not `both`: a lingering transform would offset position:fixed overlays */ + animation: card-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +@keyframes card-in { + from { + opacity: 0; + transform: translateY(8px) scale(0.992); + } + to { + opacity: 1; + transform: none; + } +} + +/* --- Window Resize Handles --- */ + +/* Overlay sibling of the card; only the handles themselves capture pointer events. */ +.window-resize-layer { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 40; +} + +.window-resize-handle { + position: absolute; + pointer-events: auto; + touch-action: none; +} + +/* Corners sit above the edge handles so their diagonal cursor wins at overlaps. */ +.window-resize-handle--nw, +.window-resize-handle--ne, +.window-resize-handle--sw, +.window-resize-handle--se { + z-index: 1; +} + +/* --- Card Header (Title Bar) --- */ + +.card-header { + display: flex; + align-items: center; + gap: 14px; + padding: 13px 22px; + border-bottom: 1px solid var(--hairline); + flex-shrink: 0; + position: relative; + /* keep the header (and its theme dropdown) above the diagram tool palette */ + z-index: 30; +} + +.app-logo svg { + display: block; + height: 30px; + width: auto; +} + +.app-logo svg path { + fill: var(--logo); + transition: fill 0.4s ease; +} + +.app-divider { + width: 1px; + height: 20px; + background: var(--hairline); +} + +.app-title { + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; + color: var(--text-1); + letter-spacing: -0.01em; +} + +.app-badge { + font-family: var(--font-mono); + font-size: 9.5px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--accent); + background: var(--accent-soft); + padding: 3px 7px; + border-radius: 4px; +} + +.app-spacer { + flex: 1; +} + +.app-links { + display: flex; + align-items: center; + gap: 2px; +} + +.app-link { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 12px; + font-weight: 500; + color: var(--text-3); + text-decoration: none; + padding: 5px 9px; + border-radius: 6px; + transition: + color 0.15s, + background 0.15s; +} + +.app-link:hover { + color: var(--text-2); + background: var(--surface-hover); +} + +.app-link svg { + width: 13px; + height: 13px; + flex-shrink: 0; +} + +/* --- Title Bar Menu Bar --- */ + +.app-menubar { + display: flex; + align-items: center; + gap: 2px; +} + +.menubar-item { + font-family: inherit; + font-size: 13px; + font-weight: 500; + color: var(--text-2); + background: transparent; + border: none; + padding: 5px 10px; + border-radius: 6px; + cursor: pointer; + transition: + color 0.15s, + background 0.15s; +} + +.menubar-item:hover, +.menubar-item.active { + color: var(--text-1); + background: var(--surface-active); +} + +/* --- Title Bar Toolbar --- */ + +.title-bar-toolbar { + display: flex; + align-items: center; + gap: 2px; + margin-right: 6px; +} + +.toolbar-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text-3); + cursor: pointer; + transition: + color 0.15s, + background 0.15s; +} + +.toolbar-btn:hover { + color: var(--text-1); + background: var(--surface-hover); +} + +.toolbar-btn:active { + background: var(--surface-active); +} + +.toolbar-btn svg { + width: 14px; + height: 14px; +} + +.dirty-indicator { + width: 7px; + height: 7px; + border-radius: 50%; + background: transparent; + margin-right: 4px; + transition: + background 0.2s, + box-shadow 0.2s; +} + +.dirty-indicator.active { + background: var(--warning); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--warning) 22%, transparent); +} + +/* --- Theme Switcher --- */ + +.theme-switcher { + position: relative; +} + +.theme-trigger { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: inherit; + font-size: 12px; + font-weight: 500; + color: var(--text-2); + background: var(--surface-sunken); + border: 1px solid var(--hairline); + padding: 5px 8px 5px 7px; + border-radius: 8px; + cursor: pointer; + transition: + color 0.15s, + background 0.15s, + border-color 0.15s; +} + +.theme-trigger:hover { + color: var(--text-1); + background: var(--surface-hover); + border-color: var(--overlay-border); +} + +.theme-swatch { + width: 16px; + height: 16px; + border-radius: 5px; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12); + flex-shrink: 0; +} + +.theme-trigger .theme-mode-glyph { + width: 13px; + height: 13px; + color: var(--text-3); +} + +.theme-trigger .theme-caret { + width: 9px; + height: 9px; + color: var(--text-3); + transition: transform 0.2s ease; +} + +.theme-switcher.open .theme-caret { + transform: rotate(180deg); +} + +.theme-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 250px; + background: var(--surface); + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); + border: 1px solid var(--overlay-border); + border-radius: 12px; + box-shadow: var(--overlay-shadow); + padding: 6px; + z-index: 100; + opacity: 0; + transform: translateY(-6px) scale(0.98); + transform-origin: top right; + pointer-events: none; + transition: + opacity 0.16s ease, + transform 0.16s ease; +} + +.theme-switcher.open .theme-menu { + opacity: 1; + transform: none; + pointer-events: auto; +} + +.theme-menu-label { + font-size: 9.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--text-3); + padding: 6px 8px 5px; +} + +/* Light / dark segmented toggle */ +.mode-toggle { + display: flex; + gap: 4px; + padding: 2px; + margin: 0 4px 4px; + background: var(--surface-sunken); + border-radius: 9px; +} + +.mode-btn { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + font-family: inherit; + font-size: 12px; + font-weight: 600; + color: var(--text-3); + background: transparent; + border: none; + padding: 6px 0; + border-radius: 7px; + cursor: pointer; + transition: + color 0.13s, + background 0.13s, + box-shadow 0.13s; +} + +.mode-btn svg { + width: 14px; + height: 14px; +} + +.mode-btn:hover { + color: var(--text-1); +} + +.mode-btn.active { + color: var(--accent); + background: var(--card-bg); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); +} + +.theme-option { + display: flex; + align-items: center; + gap: 11px; + width: 100%; + text-align: left; + font-family: inherit; + background: transparent; + border: none; + border-radius: 9px; + padding: 8px; + cursor: pointer; + color: var(--text-1); + transition: background 0.13s; +} + +.theme-option:hover { + background: var(--surface-hover); +} + +.theme-option-swatch { + width: 34px; + height: 34px; + border-radius: 8px; + flex-shrink: 0; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12); + position: relative; +} + +.theme-option-text { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; +} + +.theme-option-name { + font-size: 13px; + font-weight: 600; + letter-spacing: -0.01em; +} + +.theme-option-desc { + font-size: 11px; + color: var(--text-3); +} + +.theme-option-check { + margin-left: auto; + width: 16px; + height: 16px; + color: var(--accent); + opacity: 0; + flex-shrink: 0; +} + +.theme-option.selected .theme-option-check { + opacity: 1; +} + +.theme-option.selected .theme-option-name { + color: var(--accent); +} + +/* --- Diagram Canvas --- */ + +.card-canvas { + flex: 1; + position: relative; + background: var(--canvas-bg); + min-height: 0; + transition: background 0.4s ease; + /* own stacking context so the tool palette stays below the header dropdown */ + z-index: 1; +} + +#sprotty { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +#sprotty svg { + width: 100%; + height: 100%; +} + +/* --- Card Footer (Shortcuts) --- */ + +.card-footer { + flex-shrink: 0; + border-top: 1px solid var(--hairline); +} + +.shortcuts-toggle { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 22px; + font-size: 12px; + font-weight: 500; + color: var(--text-3); + cursor: pointer; + user-select: none; +} + +.shortcuts-toggle:hover { + color: var(--text-2); +} + +.shortcuts-chevron { + display: inline-block; + font-size: 9px; + transition: transform 0.2s ease; +} + +.shortcuts-chevron.open { + transform: rotate(90deg); +} + +.shortcuts-panel { + max-height: 0; + overflow: hidden; + transition: max-height 0.25s ease; +} + +.shortcuts-panel.open { + max-height: 320px; + overflow-y: auto; +} + +.shortcuts-inner { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 4px 28px; + padding: 0 22px 14px; +} + +.shortcuts-section { + display: flex; + flex-direction: column; + gap: 1px; +} + +.shortcuts-section-title { + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-3); + padding-bottom: 2px; +} + +.shortcut-item { + display: flex; + align-items: center; + gap: 7px; + font-size: 11px; + color: var(--text-2); + line-height: 1.6; +} + +.shortcut-item kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + padding: 2px 5px; + font-family: var(--font-mono); + font-size: 9px; + font-weight: 500; + color: var(--text-2); + background: var(--surface-sunken); + border: 1px solid var(--hairline); + border-radius: 4px; + white-space: nowrap; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} diff --git a/examples/workflow-standalone/css/command-palette.css b/examples/workflow-standalone/css/command-palette.css new file mode 100644 index 0000000..273ffa6 --- /dev/null +++ b/examples/workflow-standalone/css/command-palette.css @@ -0,0 +1,116 @@ +/******************************************************************************** + * Copyright (c) 2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +.command-palette.validation.error, +.command-palette.validation.warning { + font-size: 10pt; +} + +.command-palette.validation .validation-decorator { + position: absolute; + padding: 5px; + border-radius: 5px 5px 0px 0px; + color: white; + display: flex; + align-items: flex-start; + /* let error decoration fade in */ + -webkit-animation: fadein 0.3s; + -moz-animation: fadein 0.3s; + -ms-animation: fadein 0.3s; + -o-animation: fadein 0.3s; + animation: fadein 0.3s; +} + +.command-palette.validation .validation-decorator span { + margin-right: 5px; +} + +.command-palette.validation.error input, +.command-palette.validation.error input:focus { + color: var(--glsp-error-foreground); + outline-color: var(--glsp-error-foreground); +} + +.command-palette.validation.error .validation-decorator.error { + border: 1px solid var(--glsp-error-foreground); + background-color: var(--glsp-error-foreground); +} + +.command-palette.validation.warning input, +.command-palette.validation.warning input:focus { + outline-color: var(--glsp-warning-foreground); +} + +.command-palette.validation.warning .validation-decorator.warning { + border: 1px solid var(--glsp-warning-foreground); + background-color: var(--glsp-warning-foreground); + color: black; +} + +/* --- Container, input & suggestions --- */ + +.command-palette { + font-family: var(--font-body); + border-radius: 10px; + box-shadow: var(--overlay-shadow); + overflow: hidden; +} + +.command-palette input { + font-family: var(--font-body); + font-size: 14px; + padding: 10px 14px; + border: none; + border-bottom: 1px solid var(--hairline); + outline: none; + color: var(--text-1); + background: var(--surface-solid); +} + +.command-palette input::placeholder { + color: var(--text-3); +} + +.command-palette-suggestions { + background: var(--surface-solid); + border: none; + border-radius: 0 0 10px 10px; + box-shadow: none; + font-size: 13px; + color: var(--text-2); +} + +.command-palette-suggestions > div { + padding: 6px 14px; +} + +.command-palette-suggestions .group { + background: var(--surface-sunken); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-2); + padding: 6px 14px; +} + +.command-palette-suggestions > div:hover:not(.group) { + background: var(--surface-hover); +} + +.command-palette-suggestions > div.selected { + background: var(--accent-soft); + color: var(--accent); +} diff --git a/examples/workflow-standalone/css/context-menu.css b/examples/workflow-standalone/css/context-menu.css new file mode 100644 index 0000000..c67b9ad --- /dev/null +++ b/examples/workflow-standalone/css/context-menu.css @@ -0,0 +1,95 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +.glsp-context-menu-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; +} + +.glsp-context-menu { + position: fixed; + min-width: 160px; + background: var(--surface); + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); + border: 1px solid var(--overlay-border); + border-radius: 8px; + padding: 4px 0; + box-shadow: var(--overlay-shadow); + font-family: var(--font-body); + font-size: 13px; + z-index: 1001; +} + +.glsp-context-menu-item { + display: flex; + align-items: center; + padding: 6px 12px; + cursor: pointer; + color: var(--text-1); + user-select: none; + position: relative; +} + +.glsp-context-menu-item:hover { + background: var(--surface-hover); +} + +.glsp-context-menu-item.disabled { + color: var(--text-3); + cursor: default; + pointer-events: none; +} + +.glsp-context-menu-indicator { + width: 16px; + font-size: 11px; + text-align: center; + flex-shrink: 0; + color: var(--text-2); +} + +.glsp-context-menu-label { + flex: 1; +} + +.glsp-context-menu-chevron { + font-size: 10px; + color: var(--text-3); + margin-left: 8px; +} + +.glsp-context-menu-separator { + height: 1px; + margin: 4px 0; + background: var(--hairline); +} + +/* Submenus */ +.glsp-context-menu-submenu { + display: none; + position: absolute; + left: 100%; + top: -4px; +} + +.glsp-context-menu-item.has-submenu:hover > .glsp-context-menu-submenu { + display: block; +} diff --git a/examples/workflow-standalone/css/diagram.css b/examples/workflow-standalone/css/diagram.css new file mode 100644 index 0000000..a27ebed --- /dev/null +++ b/examples/workflow-standalone/css/diagram.css @@ -0,0 +1,272 @@ +/******************************************************************************** + * Copyright (c) 2019-2021 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* + * Diagram/SVG styling. Colors resolve to theme tokens from themes.css; this only + * re-skins the base @eclipse-glsp/client and workflow-glsp styles. Overlay + * components live in the sibling files imported below (kept first for cascade order). + */ + +@import './tool-palette.css'; +@import './command-palette.css'; +@import './context-menu.css'; +@import './shortcuts.css'; +@import './feedback.css'; + +.sprotty svg { + border: none; +} + +.sprotty-graph { + font-family: var(--font-body); +} + +.sprotty-text { + font-family: var(--font-body); +} + +.sprotty-graph, +.grid-background { + background: var(--canvas-bg); +} + +.grid-background .sprotty-graph, +.grid-background.sprotty-graph { + /* feed the themed grid line color into the base grid.css variable */ + --grid-color: var(--grid-line); +} + +/* --- Nodes & Edges --- */ + +.sprotty-node { + fill: var(--node-fill); + stroke: var(--node-stroke); + /* No SVG `filter` on nodes: under the viewport zoom transform a `drop-shadow` + filter gets clipped to its rectangular region and, worse, the browser leaves a + displaced copy of the filtered node (an offset "ghost" outline) on zoom/pan. + Depth/feedback is done with crisp strokes instead, which scale cleanly. */ + filter: none; + transition: + stroke 0.15s ease, + stroke-width 0.15s ease, + opacity 0.15s ease; +} + +.sprotty-edge { + stroke: var(--edge); + stroke-linecap: round; + stroke-linejoin: round; +} + +.sprotty-edge.arrow { + fill: var(--edge); +} + +.sprotty-edge.selected { + stroke: var(--selection); +} + +.sprotty-edge.selected > .arrow { + fill: var(--selection); + stroke: var(--selection); +} + +.forkOrJoin > .sprotty-node { + fill: var(--node-fork); +} + +.forkOrJoin > .sprotty-node.selected { + stroke: var(--selection); +} + +polygon.sprotty-node { + fill: var(--node-poly-fill); + stroke: var(--node-poly-stroke); + stroke-width: 1.25; +} + +/* --- Selection & Hover Feedback --- */ + +/* crisp stroke outlines (no SVG filters) so feedback stays sharp at any zoom level */ +.sprotty-node.selected { + stroke: var(--selection); + stroke-width: 2.5px; +} + +.sprotty-node.mouseover:not(.selected) { + /* dim on hover, matching the base client / master behavior; no SVG filter so nothing + ghosts or clips under the zoom transform (the opacity transition eases it in) */ + opacity: 60%; +} + +.sprotty-edge.mouseover:not(.selected) { + stroke: var(--accent); + opacity: 85%; +} + +.sprotty-edge.mouseover:not(.selected) > .arrow { + fill: var(--accent); + stroke: var(--accent); +} + +/* --- Resize & Routing Handles --- */ + +.sprotty-resize-handle, +.sprotty-edge > .sprotty-routing-handle { + fill: var(--handle); + stroke: var(--handle-ring); + stroke-width: 1.5px; + transition: r 0.12s ease; +} + +.sprotty-resize-handle.selected, +.sprotty-resize-handle.active, +.sprotty-edge > .sprotty-routing-handle.selected { + fill: var(--handle); + r: 6px; +} + +.sprotty-resize-handle.movement-not-allowed, +.sprotty-resize-handle.resize-not-allowed { + fill: var(--glsp-error-foreground); + stroke: var(--handle-ring); +} + +.sprotty-edge > .sprotty-routing-handle.mouseover { + stroke: var(--accent); + stroke-width: 1.5; +} + +.sprotty-node.marquee { + fill: var(--accent); + opacity: 0.16; +} + +/* --- Diagram Labels --- */ + +.sprotty-graph .sprotty-label:not(.heading) { + fill: var(--diagram-label); +} + +/* --- Workflow Node Theming (re-skins workflow-glsp node fills) --- */ + +.task.automated > .sprotty-node { + fill: var(--node-automated); +} + +.task.manual > .sprotty-node { + fill: var(--node-manual); +} + +.category > .sprotty-node { + fill: var(--node-category); +} + +.category .category > .sprotty-node { + fill: var(--node-category-nested); + stroke: var(--node-category-nested-stroke); + stroke-width: 1; +} + +.task .sprotty-label, +.category .sprotty-label { + fill: var(--node-label); +} + +.task .sprotty-label.heading, +.category .sprotty-label.heading { + fill: color-mix(in srgb, var(--node-label) 82%, transparent); +} + +.icon path { + fill: color-mix(in srgb, var(--node-label) 95%, transparent); +} + +.sprotty-edge.weighted.low:not(.selected, .navigable-element, .search-highlighted), +.sprotty-edge.weighted.low:not(.selected, .navigable-element, .search-highlighted) .arrow { + stroke: var(--edge-low); +} + +.sprotty-edge.weighted.low:not(.selected, .navigable-element, .search-highlighted) .arrow { + fill: var(--edge-low); +} + +.sprotty-edge.weighted:not(.selected, .navigable-element, .search-highlighted), +.sprotty-edge.weighted:not(.selected, .navigable-element, .search-highlighted) .arrow, +.sprotty-edge.weighted.medium:not(.selected, .navigable-element, .search-highlighted), +.sprotty-edge.weighted.medium:not(.selected, .navigable-element, .search-highlighted) .arrow { + stroke: var(--edge-medium); +} + +.sprotty-edge.weighted:not(.selected, .navigable-element, .search-highlighted) .arrow, +.sprotty-edge.weighted.medium:not(.selected, .navigable-element, .search-highlighted) .arrow { + fill: var(--edge-medium); +} + +.sprotty-edge.weighted.high:not(.selected, .navigable-element, .search-highlighted), +.sprotty-edge.weighted.high:not(.selected, .navigable-element, .search-highlighted) .arrow { + stroke: var(--edge-high); +} + +.sprotty-edge.weighted.high:not(.selected, .navigable-element, .search-highlighted) .arrow { + fill: var(--edge-high); +} + +/* --- Projection / Minimap Bars --- */ + +.bordered-projection-bar { + border-color: var(--hairline); +} + +.sprotty-viewport { + border-width: 1px; + border-color: var(--node-stroke); +} + +.sprotty-projection-bar.horizontal.bordered-projection-bar { + height: 12px; +} + +.sprotty-projection-bar.vertical.bordered-projection-bar { + width: 12px; +} + +.projection-scroll-bar { + background-color: var(--text-3); + border-radius: 3px; + opacity: 0.6; +} + +.glsp-projection { + background-color: var(--text-3); + border-radius: 2px; + opacity: 0.7; +} + +.glsp-projection.sprotty-issue.sprotty-error { + background-color: var(--glsp-error-foreground); + opacity: 1; +} + +.glsp-projection.sprotty-issue.sprotty-warning { + background-color: var(--glsp-warning-foreground); + opacity: 1; +} + +.glsp-projection.sprotty-issue.sprotty-info { + background-color: var(--glsp-info-foreground); + opacity: 1; +} diff --git a/examples/workflow-standalone/css/feedback.css b/examples/workflow-standalone/css/feedback.css new file mode 100644 index 0000000..8e52a37 --- /dev/null +++ b/examples/workflow-standalone/css/feedback.css @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* Transient feedback UI: toasts, autocomplete palette, sprotty popups. */ + +/* --- Toast --- */ + +.toast { + left: 20px; + bottom: 20px; +} + +.toast-container { + font-family: var(--font-body); + background: var(--surface); + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); + color: var(--text-1); + border: 1px solid var(--overlay-border); + border-radius: 8px; + padding: 8px 14px; + font-size: 12px; + box-shadow: var(--overlay-shadow); +} + +/* --- Autocomplete Palette --- */ + +.autocomplete-palette { + font-family: var(--font-body); +} + +.autocomplete-palette input { + font-family: var(--font-body); + font-size: 14px; + padding: 10px 14px; + border: 1px solid var(--overlay-border); + border-radius: 10px; + outline: none; + color: var(--text-1); + background: var(--surface); + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); + box-shadow: var(--overlay-shadow); +} + +.autocomplete-palette input:focus { + border-color: var(--accent); + box-shadow: + var(--overlay-shadow), + 0 0 0 2px var(--accent-glow); +} + +/* --- Sprotty Popup (Tooltip) --- */ + +.sprotty-popup { + font-family: var(--font-body); + background: var(--surface); + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); + border: 1px solid var(--overlay-border); + border-radius: 8px; + box-shadow: var(--overlay-shadow); + color: var(--text-2); + font-size: 12px; + padding: 8px 12px; +} diff --git a/examples/workflow-standalone/css/shortcuts.css b/examples/workflow-standalone/css/shortcuts.css new file mode 100644 index 0000000..5bdf7cb --- /dev/null +++ b/examples/workflow-standalone/css/shortcuts.css @@ -0,0 +1,131 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* Keyboard grid + keyboard-shortcuts overlay. */ + +/* --- Keyboard Grid Overlay --- */ + +.grid-item { + border-color: var(--overlay-border); +} + +.grid-item-number { + background: var(--surface-solid); + border: 1.5px solid var(--overlay-border); + color: var(--text-2); + font-family: var(--font-mono); + font-weight: 600; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + display: flex; + align-items: center; + justify-content: center; +} + +/* --- Keyboard Shortcuts Overlay --- */ + +.keyboard-shortcuts-menu { + font-family: var(--font-body); + background: var(--surface); + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); + border: 1px solid var(--overlay-border); + border-radius: 12px; + box-shadow: var(--overlay-shadow); + color: var(--text-1); + font-size: 13px; + line-height: 1.5; +} + +.keyboard-shortcuts-menu h3 { + font-size: 14px; + font-weight: 700; + color: var(--text-1); + padding: 12px 16px; + margin: 0; + border-bottom: 1px solid var(--hairline); +} + +.keyboard-shortcuts-container { + padding: 8px 16px 16px; + color: var(--text-2); + font-size: 13px; +} + +.menu-header { + background: var(--surface-sunken); + padding: 6px 16px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-2); + border-bottom: 1px solid var(--hairline); +} + +.shortcut-entry-container { + padding: 3px 0; + color: var(--text-2); + font-size: 13px; +} + +.column-title { + font-weight: 700; + color: var(--text-1); + font-size: 13px; + padding-top: 8px; +} + +.keyboard-shortcuts-menu kbd { + background: var(--surface-sunken); + border: 1px solid var(--overlay-border); + border-radius: 4px; + color: var(--text-2); + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + padding: 2px 6px; + box-shadow: none; + text-shadow: none; +} + +#key-shortcut-close-btn { + top: 10px; + right: 10px; + background: transparent; + color: var(--text-3); + font-size: 18px; + border-radius: 6px; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +#key-shortcut-close-btn:hover { + background: var(--surface-hover); + color: var(--text-2); +} + +.shortcut-table thead th { + border-bottom: 1px solid var(--overlay-border); + font-weight: 600; + color: var(--text-2); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; +} diff --git a/examples/workflow-standalone/css/themes.css b/examples/workflow-standalone/css/themes.css new file mode 100644 index 0000000..acd0031 --- /dev/null +++ b/examples/workflow-standalone/css/themes.css @@ -0,0 +1,266 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* + * Theme layer: all tokens, keyed on data-mode (light/dark) and data-theme (5 families). + * Each family's --selection sits outside its node palette so selected outlines stay visible. + */ + +/* ---------- Cross-theme constants ---------- */ + +:root { + /* Single external font (Outfit) for the whole app, shared across all themes; mono and the + fallbacks stay on system fonts so no other web fonts are loaded. */ + --font-body: 'Outfit', system-ui, 'Segoe UI', sans-serif; + --font-display: var(--font-body); + --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --node-label: #ffffff; + --handle: var(--selection); + --handle-ring: var(--card-bg); + --danger: #e11d48; + --warning: #f59e0b; + --info: var(--accent); +} + +/* ---------- Mode neutrals ---------- */ + +:root, +[data-mode='light'] { + color-scheme: light; + + /* neutral surfaces carry a few percent of the family accent for a cohesive tint */ + --app-bg: color-mix(in srgb, var(--accent) 7%, #e9edf2); + --card-bg: color-mix(in srgb, var(--accent) 3%, #ffffff); + --card-border: rgba(15, 23, 42, 0.07); + --card-shadow: 0 1px 3px rgba(2, 6, 23, 0.08), 0 18px 44px -18px rgba(2, 6, 23, 0.22); + --hairline: rgba(15, 23, 42, 0.07); + + --canvas-bg: color-mix(in srgb, var(--accent) 5%, #f5f7fa); + --grid-line: rgba(15, 23, 42, 0.055); + + --surface: rgba(255, 255, 255, 0.86); + --surface-solid: color-mix(in srgb, var(--accent) 3%, #ffffff); + --surface-sunken: rgba(15, 23, 42, 0.04); + --surface-hover: rgba(15, 23, 42, 0.05); + --surface-active: rgba(15, 23, 42, 0.08); + --overlay-border: rgba(15, 23, 42, 0.09); + --overlay-shadow: 0 8px 32px rgba(2, 6, 23, 0.12), 0 2px 6px rgba(2, 6, 23, 0.05); + --blur: 16px; + + --text-1: #0f172a; + --text-2: #475569; + --text-3: #94a3b8; + --text-4: #b4bdca; + + --node-fill: #e2e8f0; + --node-stroke: #cbd5e1; + --node-poly-fill: #cbd5e1; + --node-poly-stroke: #94a3b8; + --node-fork: #334155; + --diagram-label: #1e293b; + --edge: #64748b; +} + +[data-mode='dark'] { + color-scheme: dark; + + /* tinted a touch more for a richer colored-dark feel */ + --app-bg: color-mix(in srgb, var(--accent) 10%, #0a0d13); + --card-bg: color-mix(in srgb, var(--accent) 7%, #11151e); + --card-border: rgba(255, 255, 255, 0.08); + --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.5), 0 22px 50px -18px rgba(0, 0, 0, 0.7); + --hairline: rgba(255, 255, 255, 0.07); + + --canvas-bg: color-mix(in srgb, var(--accent) 8%, #0c1017); + --grid-line: rgba(255, 255, 255, 0.045); + + --surface: rgba(24, 30, 41, 0.84); + --surface-solid: color-mix(in srgb, var(--accent) 8%, #181e29); + --surface-sunken: rgba(255, 255, 255, 0.045); + --surface-hover: rgba(255, 255, 255, 0.07); + --surface-active: rgba(255, 255, 255, 0.11); + --overlay-border: rgba(255, 255, 255, 0.1); + --overlay-shadow: 0 12px 40px rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.4); + --blur: 18px; + + --text-1: #e7eaf0; + --text-2: #9aa4b3; + --text-3: #6b7585; + --text-4: #4d5664; + + --node-fill: #232b39; + --node-stroke: #344052; + --node-poly-fill: #2a3344; + --node-poly-stroke: #3d4a5e; + --node-fork: #8a93a3; + --diagram-label: #cbd5e1; + --edge: #6b7585; +} + +/* ---------- Family identity ---------- */ + +:root, +[data-theme='tide'] { + --accent: #0ea5b7; + --accent-strong: #0b8c9c; + --accent-soft: rgba(14, 165, 183, 0.12); + --accent-on: #ffffff; + --accent-glow: rgba(14, 165, 183, 0.34); + --logo: #0ea5b7; + --atmosphere: + radial-gradient(900px 620px at 10% -5%, rgba(14, 165, 183, 0.14), transparent 60%), + radial-gradient(820px 600px at 102% 104%, rgba(84, 112, 184, 0.13), transparent 55%); + + --node-automated: #0d9aac; + --node-manual: #f0795a; + --node-category: #5470b8; + --node-category-nested: #6f8fce; + --node-category-nested-stroke: #3f5fa0; + --edge-low: #9db4dc; + --edge-medium: #5570b5; + --edge-high: #3a58a8; + + --selection: #7c5cff; + --selection-glow: rgba(124, 92, 255, 0.45); +} + +[data-theme='graphite'] { + --accent: #3b9eff; + --accent-strong: #5cb0ff; + --accent-soft: rgba(59, 158, 255, 0.16); + --accent-on: #051321; + --accent-glow: rgba(59, 158, 255, 0.45); + --logo: #3b9eff; + --atmosphere: + radial-gradient(1000px 720px at 0% -8%, rgba(59, 158, 255, 0.14), transparent 60%), + radial-gradient(900px 700px at 104% 106%, rgba(47, 159, 176, 0.1), transparent 55%); + + --node-automated: #3187d4; + --node-manual: #2f9fb0; + --node-category: #5b82c5; + --node-category-nested: #6f95d6; + --node-category-nested-stroke: #3f63a0; + --edge-low: #8fa6cf; + --edge-medium: #5a78bd; + --edge-high: #3d5fb0; + + --selection: #fb5fa0; + --selection-glow: rgba(251, 95, 160, 0.45); +} + +[data-theme='ember'] { + --accent: #c2410c; + --accent-strong: #9a3412; + --accent-soft: rgba(194, 65, 12, 0.12); + --accent-on: #fff7ed; + --accent-glow: rgba(194, 65, 12, 0.34); + --logo: #c2410c; + --atmosphere: + radial-gradient(900px 620px at 8% -5%, rgba(194, 65, 12, 0.14), transparent 60%), + radial-gradient(820px 600px at 104% 106%, rgba(202, 138, 4, 0.16), transparent 55%); + + --node-automated: #c2410c; + --node-manual: #4f7d6e; + --node-category: #a16207; + --node-category-nested: #bd7e1f; + --node-category-nested-stroke: #825009; + --edge-low: #c2b08f; + --edge-medium: #a3814a; + --edge-high: #c2410c; + + --selection: #0ea5e9; + --selection-glow: rgba(14, 165, 233, 0.45); +} + +[data-theme='orchid'] { + --accent: #d946ef; + --accent-strong: #e879f9; + --accent-soft: rgba(217, 70, 239, 0.18); + --accent-on: #11061a; + --accent-glow: rgba(217, 70, 239, 0.5); + --logo: #d946ef; + --atmosphere: + radial-gradient(1000px 720px at 14% -8%, rgba(217, 70, 239, 0.18), transparent 60%), + radial-gradient(900px 700px at 104% 108%, rgba(34, 206, 192, 0.13), transparent 55%); + + --node-automated: #d437e8; + --node-manual: #22cec0; + --node-category: #7c7cf0; + --node-category-nested: #9a9af6; + --node-category-nested-stroke: #5b5be0; + --edge-low: #7c6f9e; + --edge-medium: #a855f7; + --edge-high: #d946ef; + + --selection: #f5b313; + --selection-glow: rgba(245, 179, 19, 0.5); +} + +[data-theme='verdant'] { + --accent: #10b981; + --accent-strong: #0e9f6e; + --accent-soft: rgba(16, 185, 129, 0.14); + --accent-on: #04231a; + --accent-glow: rgba(16, 185, 129, 0.4); + --logo: #10b981; + --atmosphere: + radial-gradient(940px 660px at 10% -6%, rgba(16, 185, 129, 0.14), transparent 60%), + radial-gradient(840px 620px at 104% 106%, rgba(44, 122, 158, 0.13), transparent 55%); + + --node-automated: #0e9f6e; + --node-manual: #d08700; + --node-category: #2c7a9e; + --node-category-nested: #3b93b8; + --node-category-nested-stroke: #236781; + --edge-low: #8fb8a6; + --edge-medium: #3f8f6e; + --edge-high: #0e7a52; + + --selection: #8b5cf6; + --selection-glow: rgba(139, 92, 246, 0.46); +} + +/* ---------- GLSP semantic-color bridge ---------- */ + +:root { + --glsp-error-foreground: var(--danger, #e11d48); + --glsp-warning-foreground: var(--warning, #f59e0b); + --glsp-info-foreground: var(--info, #0ea5b7); + --glsp-navigation-highlight: var(--accent-glow, rgba(14, 165, 183, 0.25)); + --glsp-search-highlight: var(--accent, #0ea5b7); +} + +/* ---------- Theme-switcher swatch gradients ---------- */ +.theme-swatch[data-theme='tide'], +.theme-option[data-theme='tide'] .theme-option-swatch { + background: linear-gradient(135deg, #0ea5b7 0%, #5470b8 55%, #f0795a 100%); +} +.theme-swatch[data-theme='graphite'], +.theme-option[data-theme='graphite'] .theme-option-swatch { + background: linear-gradient(135deg, #3b9eff 0%, #2f9fb0 55%, #fb5fa0 100%); +} +.theme-swatch[data-theme='ember'], +.theme-option[data-theme='ember'] .theme-option-swatch { + background: linear-gradient(135deg, #c2410c 0%, #a16207 55%, #4f7d6e 100%); +} +.theme-swatch[data-theme='orchid'], +.theme-option[data-theme='orchid'] .theme-option-swatch { + background: linear-gradient(135deg, #d946ef 0%, #7c7cf0 55%, #22cec0 100%); +} +.theme-swatch[data-theme='verdant'], +.theme-option[data-theme='verdant'] .theme-option-swatch { + background: linear-gradient(135deg, #10b981 0%, #2c7a9e 55%, #d08700 100%); +} diff --git a/examples/workflow-standalone/css/tool-palette.css b/examples/workflow-standalone/css/tool-palette.css new file mode 100644 index 0000000..2d5ee8a --- /dev/null +++ b/examples/workflow-standalone/css/tool-palette.css @@ -0,0 +1,203 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* Tool palette re-skin. */ + +.tool-palette { + font-family: var(--font-body); + background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; + border: none; + border-radius: 12px; + box-shadow: var(--overlay-shadow); + color: var(--text-1); + font-size: 13px; + right: 40px; + top: 32px; +} + +.tool-palette.collapsible-palette { + overflow-x: visible; + overflow-y: clip; +} + +.palette-header { + background: var(--surface); + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); + border: 1px solid var(--overlay-border); + border-bottom: 1px solid var(--hairline); + border-radius: 12px 12px 0 0; + padding: 8px 10px; +} + +.header-icon { + color: var(--text-2); + font-weight: 600; + font-size: 12px; +} + +.header-tools { + gap: 2px; +} + +.header-tools i { + border: 1px solid transparent; + border-radius: 6px; + padding: 5px; + margin-right: 1px; + color: var(--text-2); +} + +.header-tools i:hover { + background: var(--surface-hover); + color: var(--text-1); +} + +.header-tools .clicked { + background: var(--accent-soft); + border: 1px solid transparent; + color: var(--accent); +} + +.palette-body { + background: var(--surface); + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); + border: 1px solid var(--overlay-border); + border-top: none; + border-radius: 0 0 12px 12px; + padding: 4px 0; +} + +.tool-group { + background: transparent; +} + +.group-header { + background: var(--surface-sunken); + color: var(--text-2); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + padding: 6px 12px; + border-bottom: 1px solid var(--hairline); + margin-top: 2px; +} + +.group-header:hover { + background: var(--surface-hover); +} + +.group-header i { + padding: 0.2em; + color: var(--text-2); + font-size: 12px; +} + +.tool-button { + background: transparent; + padding: 6px 12px 6px 24px; + border-radius: 6px; + margin: 1px 4px; + color: var(--text-1); + font-size: 13px; + position: relative; +} + +.tool-button i { + color: var(--text-2); + margin-right: 8px; + display: inline-flex; + align-items: center; +} + +.tool-button:hover { + background: var(--surface-hover); +} + +.tool-button.clicked { + background: var(--accent-soft); + color: var(--accent); +} + +.tool-button.clicked i { + color: var(--accent); +} + +.search-input { + background: var(--surface-sunken); + border: 1px solid var(--overlay-border); + border-radius: 6px; + padding: 6px 8px; + font-family: inherit; + color: var(--text-1); + font-size: 13px; +} + +.search-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-glow); +} + +.minimize-palette-button .codicon::before { + font-size: 18px; +} + +.accessibility-tool-palette .tool-button .key-shortcut, +.accessibility-tool-palette .header-tools .key-shortcut { + background: var(--surface-solid); + border: 1.5px solid var(--overlay-border); + color: var(--text-2); + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.7rem; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); +} + +.accessibility-show-shortcuts:focus-within .header-tools .key-shortcut, +.accessibility-show-shortcuts:focus-within .tool-button .key-shortcut { + display: flex; + align-items: center; + justify-content: center; +} + +.minimize-palette-button { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: var(--surface); + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); + border: 1px solid var(--overlay-border); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); + color: var(--text-2); + cursor: pointer; +} + +.minimize-palette-button:hover { + color: var(--text-1); + background: var(--surface-hover); +} + +.minimize-palette-button .codicon::before { + font-size: 16px; +} diff --git a/examples/workflow-standalone/esbuild.js b/examples/workflow-standalone/esbuild.js new file mode 100644 index 0000000..5b5b622 --- /dev/null +++ b/examples/workflow-standalone/esbuild.js @@ -0,0 +1,136 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +// @ts-check +const { spawn } = require('child_process'); +const esbuild = require('esbuild'); +const fs = require('fs'); +const path = require('path'); + +const appRoot = path.resolve(__dirname, 'app'); +const serverDir = path.resolve(__dirname, 'server'); + +const args = process.argv.slice(2); +const isBrowser = args.includes('--browser'); +const isWatch = args.includes('--watch'); // dev: rebuild + live-reload +const isServe = args.includes('--serve'); // start: serve the built bundle, no watch/live-reload +const isMcp = args.includes('--mcp'); +const noOpen = args.includes('--no-open'); + +// full-page live reload: subscribe to esbuild's change stream (served on the dev port). Over file:// +// there is no EventSource endpoint, so the guard turns this into a harmless no-op for production builds. +const liveReloadBanner = { + js: ";(() => { if (typeof EventSource !== 'undefined') { new EventSource('/esbuild').addEventListener('change', () => location.reload()); } })();" +}; + +/** + * Reports the build progress and surfaces errors/warnings in a format that + * VS Code's `$esbuild-watch` problem matcher can pick up. + * @type {import('esbuild').Plugin} + */ +const esbuildProblemMatcherPlugin = { + name: 'esbuild-problem-matcher', + setup(build) { + build.onStart(() => { + console.log(`${isWatch ? '[watch] ' : ''}build started`); + }); + build.onEnd(result => { + result.errors.forEach(({ text, location }) => { + console.error(`✘ [ERROR] ${text}`); + if (location) { + console.error(` ${location.file}:${location.line}:${location.column}:`); + } + }); + console.log(`${isWatch ? '[watch] ' : ''}build finished`); + }); + } +}; + +// replaces CopyWebpackPlugin: the browser entry loads the worker via the stable 'wf-glsp-server-webworker.js' name +function copyWebWorker() { + const source = path.resolve(serverDir, 'wf-glsp-server-web.js'); + const target = path.resolve(appRoot, 'wf-glsp-server-webworker.js'); + fs.copyFileSync(source, target); + if (fs.existsSync(source + '.map')) { + fs.copyFileSync(source + '.map', target + '.map'); + } +} + +// mirror webpack's DefinePlugin; only injected for the node/websocket entry +const nodeDefine = { + GLSP_SERVER_HOST: JSON.stringify(process.env.GLSP_SERVER_HOST || 'localhost'), + GLSP_SERVER_PORT: JSON.stringify(process.env.GLSP_SERVER_PORT || '8081'), + GLSP_MCP_SERVER_PORT: JSON.stringify(process.env.GLSP_MCP_SERVER_PORT || '64577'), + GLSP_SOURCE_URI: JSON.stringify(path.resolve(appRoot, 'example1.wf')) +}; + +/** @type {import('esbuild').BuildOptions} */ +const buildOptions = { + entryPoints: [path.resolve(__dirname, 'src', isBrowser ? 'browser/app.ts' : 'node/app.ts')], + outdir: appRoot, + entryNames: 'bundle', // -> app/bundle.js + app/bundle.css + assetNames: '[name]-[hash]', // -> app/codicon-.ttf, referenced relatively from bundle.css + bundle: true, + sourcemap: true, + format: 'iife', // diagram.html loads bundle.js via a classic