feat: init flowgram.ai
Co-Authored-By: xiamidaxia <xiamidaxia@icloud.com>
14
.gitattributes
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Don't allow people to merge changes to these generated files, because the result
|
||||||
|
# may be invalid. You need to run "rush update" again.
|
||||||
|
pnpm-lock.yaml merge=ours
|
||||||
|
shrinkwrap.yaml merge=binary
|
||||||
|
npm-shrinkwrap.json merge=binary
|
||||||
|
yarn.lock merge=binary
|
||||||
|
|
||||||
|
# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic
|
||||||
|
# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor
|
||||||
|
# may also require a special configuration to allow comments in JSON.
|
||||||
|
#
|
||||||
|
# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088
|
||||||
|
#
|
||||||
|
*.json linguist-language=JSON-with-Comments
|
||||||
7
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# 文件路径与代码负责人分配
|
||||||
|
# 对整个仓库设置代码负责人
|
||||||
|
* @xukai.luics @wencheng.lwc
|
||||||
|
|
||||||
|
# 对特定目录设置代码负责人
|
||||||
|
/common/ @wencheng.lwc @chenjiawei.inizio
|
||||||
|
/config/ @wencheng.lwc @chenjiawei.inizio
|
||||||
32
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
- name: Config Git User
|
||||||
|
run: |
|
||||||
|
git config --local user.name "chenjiawei.inizio"
|
||||||
|
git config --local user.email "chenjiawei.inizio@bytedance.com"
|
||||||
|
- name: For Debug
|
||||||
|
run: |
|
||||||
|
echo "Listing files in the root directory:"
|
||||||
|
ls -alh
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
- name: Verify Change Logs
|
||||||
|
run: node common/scripts/install-run-rush.js change --verify
|
||||||
|
- name: Rush Install
|
||||||
|
run: node common/scripts/install-run-rush.js install
|
||||||
|
- name: Rush build
|
||||||
|
run: node common/scripts/install-run-rush.js build
|
||||||
|
# - name: Test (coverage)
|
||||||
|
# run: node common/scripts/install-run-rush.js test:cov -v
|
||||||
63
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
name: Publish
|
||||||
|
on:
|
||||||
|
workflow_dispatch
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "main-branch-workflow" # 唯一标识符,确保只运行一个实例
|
||||||
|
cancel-in-progress: false # 不取消正在运行的实例,后续触发需要等待当前实例完成
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
- name: Set up npm token
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||||
|
run: echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
|
||||||
|
- name: Debug Auth
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
npm whoami
|
||||||
|
- name: Config Git User
|
||||||
|
run: |
|
||||||
|
git config --local user.name "chenjiawei.inizio"
|
||||||
|
git config --local user.email "chenjiawei.inizio@bytedance.com"
|
||||||
|
- name: Get latest npm version
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
LATEST_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)
|
||||||
|
echo "LATEST_VERSION=$LATEST_VERSION" >> $GITHUB_ENV
|
||||||
|
echo "::set-output name=version::$LATEST_VERSION"
|
||||||
|
- name: Echo version
|
||||||
|
run: |
|
||||||
|
echo "The package version is : $LATEST_VERSION"
|
||||||
|
echo "The package output version is ${{ steps.get_version.outputs.version }}"
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
registry-url: 'https://registry.npmjs.org/'
|
||||||
|
- name: Rush Install
|
||||||
|
run: node common/scripts/install-run-rush.js install
|
||||||
|
- name: Rush build
|
||||||
|
run: node common/scripts/install-run-rush.js build
|
||||||
|
# version bump 之前保证是远端最新的,这样无需 commit package.json version
|
||||||
|
- name: Sync versions
|
||||||
|
run: |
|
||||||
|
echo "[
|
||||||
|
{
|
||||||
|
\"policyName\": \"publishPolicy\",
|
||||||
|
\"definitionName\": \"lockStepVersion\",
|
||||||
|
\"version\": \"$LATEST_VERSION\",
|
||||||
|
\"nextBump\": \"patch\"
|
||||||
|
}
|
||||||
|
]" > common/config/rush/version-policies.json
|
||||||
|
- name: Version Bump
|
||||||
|
run: node common/scripts/install-run-rush.js version --bump
|
||||||
|
- name: Publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||||
|
run: node common/scripts/install-run-rush.js publish --include-all -p --tag latest
|
||||||
125
.gitignore
vendored
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov/
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt/
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components/
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript/
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release/
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm/
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next/
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus/
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache/
|
||||||
|
.yarn/unplugged/
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# OS X temporary files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# IntelliJ IDEA project files; if you want to commit IntelliJ settings, this recipe may be helpful:
|
||||||
|
# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
|
||||||
|
# Rush temporary files
|
||||||
|
common/deploy/
|
||||||
|
common/temp/
|
||||||
|
common/autoinstallers/*/.npmrc
|
||||||
|
**/.rush/temp/
|
||||||
|
*.lock
|
||||||
|
|
||||||
|
# Common toolchain intermediate files
|
||||||
|
temp/
|
||||||
|
lib/
|
||||||
|
lib-amd/
|
||||||
|
lib-es6/
|
||||||
|
lib-esnext/
|
||||||
|
lib-commonjs/
|
||||||
|
lib-shim/
|
||||||
|
dist/
|
||||||
|
dist-storybook/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Heft temporary files
|
||||||
|
.cache/
|
||||||
|
.heft/
|
||||||
|
|
||||||
|
# rush standard files
|
||||||
|
.eslintcache
|
||||||
108
CONTRIBUTING.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# Contributing to flowgram.ai
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+ (LTS/Hydrogen recommended)
|
||||||
|
- pnpm 9.12.0
|
||||||
|
- Rush 5.14.0
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Install Node.js 18+**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
nvm install lts/hydrogen
|
||||||
|
nvm alias default lts/hydrogen # set default node version
|
||||||
|
nvm use lts/hydrogen
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Clone the repository**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
git clone git@github.com:coze-dev/flowgram.ai.git
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install required global dependencies**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
npm i -g pnpm@9.12.0 @microsoft/rush@5.14.0
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Install project dependencies**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
rush update
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Build the project**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
rush build
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Run docs or demo**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
rush dev:docs # docs
|
||||||
|
rush dev:demo-fixed-layout
|
||||||
|
rush dev:demo-free-layout
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, you can start to develop projects inside this repository.
|
||||||
|
|
||||||
|
|
||||||
|
## Submitting Changes
|
||||||
|
|
||||||
|
1. Create a new branch from `main` using the format:
|
||||||
|
- `feat/description` for features
|
||||||
|
- `fix/description` for bug fixes
|
||||||
|
- `docs/description` for documentation
|
||||||
|
- `chore/description` for maintenance
|
||||||
|
|
||||||
|
2. Write code and tests
|
||||||
|
- Follow our coding standards
|
||||||
|
- Add/update tests for changes
|
||||||
|
- Update documentation if needed
|
||||||
|
|
||||||
|
3. Ensure quality
|
||||||
|
- Run `cd path/to/packageName && npm test` for all tests
|
||||||
|
- Run `rush lint` for code style
|
||||||
|
- Run `rush build` to verify build
|
||||||
|
|
||||||
|
4. Create Pull Request
|
||||||
|
- Use the PR template
|
||||||
|
- Link related issues
|
||||||
|
- Provide clear description of changes
|
||||||
|
|
||||||
|
5. Review Process
|
||||||
|
- Maintainers will review your PR
|
||||||
|
- Address review feedback if any
|
||||||
|
- Changes must pass CI checks
|
||||||
|
|
||||||
|
6. Commit Message Format
|
||||||
|
```
|
||||||
|
type(scope): subject
|
||||||
|
body
|
||||||
|
```
|
||||||
|
Types: feat, fix, docs, style, refactor, test, chore
|
||||||
|
|
||||||
|
## Reporting Bugs
|
||||||
|
|
||||||
|
Report bugs via [GitHub Issues](https://github.com/coze-dev/flowgram.ai/issues/new/choose). Please include:
|
||||||
|
|
||||||
|
- Issue description
|
||||||
|
- Steps to reproduce
|
||||||
|
- Expected behavior
|
||||||
|
- Actual behavior
|
||||||
|
- Code examples (if applicable)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- Update API documentation for interface changes
|
||||||
|
- Update README.md if usage is affected
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is under the [MIT License](http://choosealicense.com/licenses/mit/). By submitting code, you agree to these terms.
|
||||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Spring (SG) Pte. Ltd.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
124
README.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# FlowGram.AI
|
||||||
|
|
||||||
|
FlowGram is a node-based flow building engine that helps developers quickly create workflows in either fixed layout or
|
||||||
|
free connection layout modes. It provides a set of interaction best practices and is particularly suitable for visual
|
||||||
|
workflows with clear inputs and outputs.
|
||||||
|
|
||||||
|
In the current AI boom, we are also focusing on how to empower workflows with AI capabilities, hence the AI suffix in
|
||||||
|
our name.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://github.com/coze-dev/flowgram.ai/blob/main/LICENSE)
|
||||||
|
[](https://www.npmjs.com/package/@flowgram.ai/editor)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 📖 Documentation
|
||||||
|
|
||||||
|
- [Official Documentation](https://coze-dev.github.io/flowgram.ai/)
|
||||||
|
- [Contributing Guidelines](https://github.com/coze-dev/flowgram.ai/blob/main/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## 📦 Packages
|
||||||
|
|
||||||
|
| Package | Description | Version |
|
||||||
|
|---------------------------------------------------------------------------|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| [@flowgram.ai/create-app](./apps/create-app) | App Creator | [](https://www.npmjs.com/package/@flowgram.ai/create-app) |
|
||||||
|
| [@flowgram.ai/fixed-layout-editor](./packages/client/fixed-layout-editor) | Fixed Layout Editor | [](https://www.npmjs.com/package/@flowgram.ai/fixed-layout-editor) |
|
||||||
|
| [@flowgram.ai/free-layout-editor](./packages/client/free-layout-editor) | Free Layout Editor | [](https://www.npmjs.com/package/@flowgram.ai/free-layout-editor) |
|
||||||
|
|
||||||
|
## 🎮 Examples
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<a href="https://coze-dev.github.io/flowgram.ai/examples/fixed-layout/fixed-feature-overview.html">
|
||||||
|
Fixed Layout
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Fixed layout where nodes can be dragged to specified positions, with support for compound nodes like branches and loops.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<img src="./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif"/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<a href="https://coze-dev.github.io/flowgram.ai/examples/free-layout/free-feature-overview.html">
|
||||||
|
Free Layout
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Free layout where nodes can be moved anywhere and connected through free-form lines.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<img src="./apps/docs/src/public/free-layout/free-layout-demo.gif"/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# create demo
|
||||||
|
npx @flowgram.ai/create-app@latest
|
||||||
|
|
||||||
|
# select demo
|
||||||
|
- fixed-layout # full-feature overview
|
||||||
|
- free-layout # full-feature overview
|
||||||
|
- fixed-layout-simple # basic usage
|
||||||
|
- free-layout-simple # basic usage
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔨 Development
|
||||||
|
|
||||||
|
1. **Install Node.js 18+**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
nvm install lts/hydrogen
|
||||||
|
nvm alias default lts/hydrogen # set default node version
|
||||||
|
nvm use lts/hydrogen
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Clone the repository**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
git clone git@github.com:coze-dev/flowgram.ai.git
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install required global dependencies**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
npm i -g pnpm@9.12.0 @microsoft/rush@5.140.0
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Install project dependencies**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
rush update
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Build the project**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
rush build
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Run docs or demo**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
rush dev:docs # docs
|
||||||
|
rush dev:demo-fixed-layout
|
||||||
|
rush dev:demo-free-layout
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, you can start to develop projects inside this repository.
|
||||||
|
|
||||||
|
Enjoy it!
|
||||||
|
|
||||||
|
## 🌟 Contributors
|
||||||
|
|
||||||
|
[](https://github.com/coze-dev/flowgram.ai/graphs/contributors)
|
||||||
|
|
||||||
|
<!-- ## Star History
|
||||||
|
[](https://api.star-history.com/svg?repos=coze-dev/flowgram.ai&type=Timeline) -->
|
||||||
74
apps/create-app/.eslintrc.cjs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
requireConfigFile: false,
|
||||||
|
babelOptions: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
cwd: __dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/__mocks__',
|
||||||
|
'**/node_modules',
|
||||||
|
'**/build',
|
||||||
|
'**/dist',
|
||||||
|
'**/es',
|
||||||
|
'**/lib',
|
||||||
|
'**/.codebase',
|
||||||
|
'**/.changeset',
|
||||||
|
'**/config',
|
||||||
|
'**/common/scripts',
|
||||||
|
'**/output',
|
||||||
|
'error-log-str.js',
|
||||||
|
'*.bundle.js',
|
||||||
|
'*.min.js',
|
||||||
|
'*.js.map',
|
||||||
|
'**/output',
|
||||||
|
'**/*.log',
|
||||||
|
'**/tsconfig.tsbuildinfo',
|
||||||
|
'**/vitest.config.ts',
|
||||||
|
'package.json',
|
||||||
|
'*.json',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
'react/no-deprecated': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'lines-between-class-members': 'warn',
|
||||||
|
'react/jsx-no-useless-fragment': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
'no-empty-fuNction': 'off',
|
||||||
|
'prefer-destructurin': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'no-empty-function': 'off',
|
||||||
|
'no-multi-assign': 'off',
|
||||||
|
'arrow-body-style': 'warn',
|
||||||
|
'no-useless-constructor': 'off',
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
'max-classes-per-file': 'off',
|
||||||
|
'grouped-accessor-pairs': 'off',
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-restricted-syntax': 'off',
|
||||||
|
'react/destructuring-assignment': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'consistent-return': 'off',
|
||||||
|
'jsx-a11y/no-static-element-interactions': 'off',
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
'no-bitwise': 'off',
|
||||||
|
'no-case-declarations': 'off',
|
||||||
|
'react/no-array-index-key': 'off',
|
||||||
|
'react/require-default-props': 'off',
|
||||||
|
'no-dupe-class-members': 'off',
|
||||||
|
'react/jsx-props-no-spreading': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
'default-param-last': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
},
|
||||||
|
}
|
||||||
2
apps/create-app/bin/index.js
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import '../dist/index.js';
|
||||||
40
apps/create-app/package.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@flowgram.ai/create-app",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"description": "A CLI tool to create demo projects",
|
||||||
|
"bin": {
|
||||||
|
"create-app": "./bin/index.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"bin",
|
||||||
|
"src",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup src/index.ts --format esm,cjs --dts --out-dir dist",
|
||||||
|
"start": "node bin/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fs-extra": "^9.1.0",
|
||||||
|
"commander": "^11.0.0",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"download": "8.0.0",
|
||||||
|
"tar": "7.4.3",
|
||||||
|
"inquirer": "^9.2.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/download": "8.0.5",
|
||||||
|
"@types/fs-extra": "11.0.4",
|
||||||
|
"@types/node": "^18",
|
||||||
|
"@types/inquirer": "9.0.7",
|
||||||
|
"tsup": "^8.0.1",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"@typescript-eslint/parser": "^6.10.0",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
}
|
||||||
|
}
|
||||||
103
apps/create-app/src/index.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import inquirer from 'inquirer';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import download from 'download';
|
||||||
|
import * as tar from 'tar';
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('1.0.0')
|
||||||
|
.description('Create a demo project')
|
||||||
|
.action(async () => {
|
||||||
|
console.log(chalk.green('Welcome to @flowgram.ai/create-app CLI!'));
|
||||||
|
const latest = execSync('npm view @flowgram.ai/core version --tag=latest latest').toString().trim();
|
||||||
|
|
||||||
|
let folderName = ''
|
||||||
|
|
||||||
|
if (!args?.length) {
|
||||||
|
// 询问用户选择 demo 项目
|
||||||
|
const { repo } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'repo',
|
||||||
|
message: 'Select a demo to create:',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Fixed Layout Demo', value: 'demo-fixed-layout' },
|
||||||
|
{ name: 'Free Layout Demo', value: 'demo-free-layout' },
|
||||||
|
{ name: 'Fixed Layout Demo Simple', value: 'demo-fixed-layout-simple' },
|
||||||
|
{ name: 'Free Layout Demo Simple', value: 'demo-free-layout-simple' }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
folderName = repo;
|
||||||
|
} else {
|
||||||
|
if (['demo-fixed-layout', 'demo-free-layout', 'demo-fixed-layout-simple', 'demo-free-layout-simple'].includes(args[0])) {
|
||||||
|
folderName = args[0];
|
||||||
|
} else {
|
||||||
|
console.error('Invalid argument. Please run "npx create-app" to choose demo.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const targetDir = path.join(process.cwd());
|
||||||
|
// 下载 npm 包的 tarball
|
||||||
|
const downloadPackage = async () => {
|
||||||
|
try {
|
||||||
|
// 从 npm registry 下载 tarball 文件
|
||||||
|
const tarballBuffer = await download(`https://registry.npmjs.org/@flowgram.ai/${folderName}/-/${folderName}-${latest}.tgz`);
|
||||||
|
|
||||||
|
// 确保目标文件夹存在
|
||||||
|
fs.ensureDirSync(targetDir);
|
||||||
|
|
||||||
|
// 创建一个临时文件名来保存 tarball 数据
|
||||||
|
const tempTarballPath = path.join(process.cwd(), `${folderName}.tgz`);
|
||||||
|
|
||||||
|
// 将下载的 tarball 写入临时文件
|
||||||
|
fs.writeFileSync(tempTarballPath, tarballBuffer);
|
||||||
|
|
||||||
|
// 解压 tarball 文件到目标文件夹
|
||||||
|
await tar.x({
|
||||||
|
file: tempTarballPath,
|
||||||
|
C: targetDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.renameSync(path.join(targetDir, 'package'), path.join(targetDir, folderName))
|
||||||
|
|
||||||
|
// 删除下载的 tarball 文件
|
||||||
|
fs.unlinkSync(tempTarballPath);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error downloading or extracting package: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const res = await downloadPackage();
|
||||||
|
if (res) {
|
||||||
|
// 克隆项目
|
||||||
|
console.log(chalk.green(`${folderName} Demo project created successfully!`));
|
||||||
|
|
||||||
|
console.log(chalk.yellow('Run the following commands to start:'));
|
||||||
|
console.log(chalk.cyan(` cd ${folderName}`));
|
||||||
|
console.log(chalk.cyan(' npm install'));
|
||||||
|
console.log(chalk.cyan(' npm start'));
|
||||||
|
} else {
|
||||||
|
console.log(chalk.red('Download failed'))
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading repo:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
20
apps/create-app/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "esnext",
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["es6", "dom", "es2020", "es2019.Array"]
|
||||||
|
},
|
||||||
|
"include": ["./src"],
|
||||||
|
}
|
||||||
74
apps/demo-fixed-layout-simple/.eslintrc.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
requireConfigFile: false,
|
||||||
|
babelOptions: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
cwd: __dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/__mocks__',
|
||||||
|
'**/node_modules',
|
||||||
|
'**/build',
|
||||||
|
'**/dist',
|
||||||
|
'**/es',
|
||||||
|
'**/lib',
|
||||||
|
'**/.codebase',
|
||||||
|
'**/.changeset',
|
||||||
|
'**/config',
|
||||||
|
'**/common/scripts',
|
||||||
|
'**/output',
|
||||||
|
'error-log-str.js',
|
||||||
|
'*.bundle.js',
|
||||||
|
'*.min.js',
|
||||||
|
'*.js.map',
|
||||||
|
'**/output',
|
||||||
|
'**/*.log',
|
||||||
|
'**/tsconfig.tsbuildinfo',
|
||||||
|
'**/vitest.config.ts',
|
||||||
|
'package.json',
|
||||||
|
'*.json',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
'react/no-deprecated': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'lines-between-class-members': 'warn',
|
||||||
|
'react/jsx-no-useless-fragment': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
'no-empty-fuNction': 'off',
|
||||||
|
'prefer-destructurin': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'no-empty-function': 'off',
|
||||||
|
'no-multi-assign': 'off',
|
||||||
|
'arrow-body-style': 'warn',
|
||||||
|
'no-useless-constructor': 'off',
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
'max-classes-per-file': 'off',
|
||||||
|
'grouped-accessor-pairs': 'off',
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-restricted-syntax': 'off',
|
||||||
|
'react/destructuring-assignment': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'consistent-return': 'off',
|
||||||
|
'jsx-a11y/no-static-element-interactions': 'off',
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
'no-bitwise': 'off',
|
||||||
|
'no-case-declarations': 'off',
|
||||||
|
'react/no-array-index-key': 'off',
|
||||||
|
'react/require-default-props': 'off',
|
||||||
|
'no-dupe-class-members': 'off',
|
||||||
|
'react/jsx-props-no-spreading': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
'default-param-last': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
},
|
||||||
|
}
|
||||||
12
apps/demo-fixed-layout-simple/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bundler="rspack">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Flow FixedLayoutEditor Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
54
apps/demo-fixed-layout-simple/package.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@flowgram.ai/demo-fixed-layout-simple",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"files": [
|
||||||
|
"src/",
|
||||||
|
".eslintrc.js",
|
||||||
|
".gitignore",
|
||||||
|
"index.html",
|
||||||
|
"package.json",
|
||||||
|
"rspack.config.js",
|
||||||
|
"tsconfig.json"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "exit 0",
|
||||||
|
"build:fast": "exit 0",
|
||||||
|
"build:watch": "exit 0",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"dev": "MODE=app NODE_ENV=development rspack serve",
|
||||||
|
"lint": "eslint ./src --cache",
|
||||||
|
"lint:fix": "eslint ./src --fix",
|
||||||
|
"start": "NODE_ENV=development rspack serve",
|
||||||
|
"test": "exit",
|
||||||
|
"test:cov": "exit",
|
||||||
|
"watch": "exit 0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@douyinfe/semi-icons": "^2.72.3",
|
||||||
|
"@douyinfe/semi-ui": "^2.72.3",
|
||||||
|
"@flowgram.ai/fixed-layout-editor": "workspace:*",
|
||||||
|
"@flowgram.ai/fixed-semi-materials": "workspace:*",
|
||||||
|
"@flowgram.ai/minimap-plugin": "workspace:*",
|
||||||
|
"nanoid": "^4.0.2",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rspack/cli": "0.2.1",
|
||||||
|
"@types/node": "^18",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"@typescript-eslint/parser": "^6.10.0",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"less": "^4.1.2",
|
||||||
|
"less-loader": "^6"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
}
|
||||||
|
}
|
||||||
64
apps/demo-fixed-layout-simple/rspack.config.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const isCI = process.env.CI === 'true';
|
||||||
|
const isSCM = !!process.env.BUILD_BRANCH;
|
||||||
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
/**
|
||||||
|
* @type {import('@rspack/cli').Configuration}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
mode: process.env.NODE_ENV,
|
||||||
|
context: __dirname,
|
||||||
|
target: ['web'],
|
||||||
|
entry: {
|
||||||
|
main: './src/app.tsx',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
react: require.resolve('react'),
|
||||||
|
'react-dom': require.resolve('react-dom'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
builtins: {
|
||||||
|
// https://www.rspack.dev/config/builtins.html#builtinshtml
|
||||||
|
html: [
|
||||||
|
{
|
||||||
|
template: './index.html',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
progress: !isSCM ? {} : false,
|
||||||
|
treeShaking: isProd,
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
// https://www.rspack.dev/config/module.html#rule
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(png|gif|jpg|jpeg|svg|woff2)$/,
|
||||||
|
type: 'asset',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(less|css)$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'less-loader',
|
||||||
|
options: {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'css',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
/** module is too large now, we may need better way to tackle this in the future */
|
||||||
|
stats: isCI
|
||||||
|
? { all: false, modules: true, assets: true, chunks: true, warnings: true, errors: true }
|
||||||
|
: {
|
||||||
|
modules: false,
|
||||||
|
all: false,
|
||||||
|
warnings: false,
|
||||||
|
errors: true,
|
||||||
|
timings: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
5
apps/demo-fixed-layout-simple/src/app.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Editor } from './editor'
|
||||||
|
|
||||||
|
|
||||||
|
ReactDOM.render(<Editor />, document.getElementById('root'));
|
||||||
32
apps/demo-fixed-layout-simple/src/components/base-node.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
|
||||||
|
/**
|
||||||
|
* Provides methods related to node rendering
|
||||||
|
* 提供节点渲染相关的方法
|
||||||
|
*/
|
||||||
|
const nodeRender = useNodeRender();
|
||||||
|
/**
|
||||||
|
* It can only be used when nodeEngine is enabled
|
||||||
|
* 只有在节点引擎开启时候才能使用表单
|
||||||
|
*/
|
||||||
|
const form = nodeRender.form;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="demo-fixed-node"
|
||||||
|
onMouseEnter={nodeRender.onMouseEnter}
|
||||||
|
onMouseLeave={nodeRender.onMouseLeave}
|
||||||
|
onMouseDown={e => {
|
||||||
|
// trigger drag node
|
||||||
|
nodeRender.startDrag(e);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{form?.render()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { type FlowNodeEntity, useClientContext } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { IconPlus } from '@douyinfe/semi-icons';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
|
||||||
|
interface PropsType {
|
||||||
|
activated?: boolean;
|
||||||
|
node: FlowNodeEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function BranchAdder(props: PropsType) {
|
||||||
|
const { activated, node } = props;
|
||||||
|
const nodeData = node.firstChild!.renderData;
|
||||||
|
const ctx = useClientContext();
|
||||||
|
const { operation, playground } = ctx;
|
||||||
|
const { isVertical } = node;
|
||||||
|
|
||||||
|
function addBranch() {
|
||||||
|
const block = operation.addBlock(node, {
|
||||||
|
id: `branch_${nanoid(5)}`,
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'New Branch',
|
||||||
|
content: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
playground.scrollToView({
|
||||||
|
bounds: block.bounds,
|
||||||
|
scrollToCenter: true,
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
if (playground.config.readonlyOrDisabled) return null;
|
||||||
|
|
||||||
|
const className = [
|
||||||
|
'demo-fixed-adder',
|
||||||
|
isVertical ? '' : 'isHorizontal',
|
||||||
|
activated ? 'activated' : ''
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
onMouseEnter={() => nodeData?.toggleMouseEnter()}
|
||||||
|
onMouseLeave={() => nodeData?.toggleMouseLeave()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
addBranch();
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ flexGrow: 1, textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
<IconPlus />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
apps/demo-fixed-layout-simple/src/components/minimap.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { FlowMinimapService, MinimapRender } from '@flowgram.ai/minimap-plugin';
|
||||||
|
import { useService } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
|
||||||
|
export const Minimap = () => {
|
||||||
|
const minimapService = useService(FlowMinimapService);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 16,
|
||||||
|
bottom: 51,
|
||||||
|
zIndex: 100,
|
||||||
|
width: 182,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MinimapRender
|
||||||
|
service={minimapService}
|
||||||
|
containerStyles={{
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
top: 'unset',
|
||||||
|
right: 'unset',
|
||||||
|
bottom: 'unset',
|
||||||
|
left: 'unset',
|
||||||
|
}}
|
||||||
|
inactiveStyle={{
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
translateX: 0,
|
||||||
|
translateY: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
67
apps/demo-fixed-layout-simple/src/components/node-adder.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { FlowNodeEntity, FlowOperationService, useClientContext, usePlayground, useService } from "@flowgram.ai/fixed-layout-editor"
|
||||||
|
|
||||||
|
import { Dropdown } from '@douyinfe/semi-ui'
|
||||||
|
|
||||||
|
import { IconPlusCircle } from "@douyinfe/semi-icons";
|
||||||
|
import { nodeRegistries } from '../node-registries';
|
||||||
|
|
||||||
|
export const NodeAdder = (props: {
|
||||||
|
from: FlowNodeEntity;
|
||||||
|
to?: FlowNodeEntity;
|
||||||
|
hoverActivated: boolean;
|
||||||
|
}) => {
|
||||||
|
const { from, hoverActivated } = props;
|
||||||
|
const playground = usePlayground();
|
||||||
|
const context = useClientContext();
|
||||||
|
const flowOperationService = useService(FlowOperationService) as FlowOperationService;
|
||||||
|
|
||||||
|
const add = (addProps: any) => {
|
||||||
|
const blocks = addProps.blocks ? addProps.blocks : undefined;
|
||||||
|
const block = flowOperationService.addFromNode(from, {
|
||||||
|
...addProps,
|
||||||
|
blocks,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
playground.scrollToView({
|
||||||
|
bounds: block.bounds,
|
||||||
|
scrollToCenter: true,
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (playground.config.readonlyOrDisabled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
render={
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{nodeRegistries.map(registry => <Dropdown.Item onClick={() => {
|
||||||
|
const props = registry?.onAdd(context, from);
|
||||||
|
add(props);
|
||||||
|
}}>{registry.type}</Dropdown.Item>)}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: hoverActivated ? 15 : 6,
|
||||||
|
height: hoverActivated ? 15 : 6,
|
||||||
|
backgroundColor: 'rgb(143, 149, 158)',
|
||||||
|
color: '#fff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hoverActivated ?
|
||||||
|
<IconPlusCircle
|
||||||
|
style={{
|
||||||
|
color: '#3370ff',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 15
|
||||||
|
}}
|
||||||
|
/> : null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
apps/demo-fixed-layout-simple/src/components/tools.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
export function Tools() {
|
||||||
|
const { history } = useClientContext();
|
||||||
|
const tools = usePlaygroundTools();
|
||||||
|
const [canUndo, setCanUndo] = useState(false);
|
||||||
|
const [canRedo, setCanRedo] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const disposable = history.undoRedoService.onChange(() => {
|
||||||
|
setCanUndo(history.canUndo());
|
||||||
|
setCanRedo(history.canRedo());
|
||||||
|
});
|
||||||
|
return () => disposable.dispose();
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
return <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}>
|
||||||
|
<button onClick={() => tools.zoomin()}>ZoomIn</button>
|
||||||
|
<button onClick={() => tools.zoomout()}>ZoomOut</button>
|
||||||
|
<button onClick={() => tools.fitView()}>Fitview</button>
|
||||||
|
<button onClick={() => tools.changeLayout()}>ChangeLayout</button>
|
||||||
|
<button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>
|
||||||
|
<button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>
|
||||||
|
<span>{Math.floor(tools.zoom * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
23
apps/demo-fixed-layout-simple/src/editor.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import '@flowgram.ai/fixed-layout-editor/index.css';
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
import { useEditorProps } from './hooks/use-editor-props';
|
||||||
|
import { initialData } from './initial-data'
|
||||||
|
import { nodeRegistries } from './node-registries'
|
||||||
|
import { Tools } from './components/tools'
|
||||||
|
import { Minimap } from './components/minimap'
|
||||||
|
|
||||||
|
export const Editor = () => {
|
||||||
|
const editorProps = useEditorProps(initialData, nodeRegistries);
|
||||||
|
return (
|
||||||
|
<FixedLayoutEditorProvider {...editorProps}>
|
||||||
|
<div className="demo-fixed-container">
|
||||||
|
<EditorRenderer>{/* add child panel here */}</EditorRenderer>
|
||||||
|
</div>
|
||||||
|
<Tools />
|
||||||
|
<Minimap />
|
||||||
|
</FixedLayoutEditorProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
154
apps/demo-fixed-layout-simple/src/hooks/use-editor-props.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
|
||||||
|
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
|
||||||
|
import {
|
||||||
|
type FixedLayoutProps,
|
||||||
|
FlowDocumentJSON,
|
||||||
|
FlowNodeRegistry,
|
||||||
|
FlowTextKey,
|
||||||
|
Field,
|
||||||
|
FlowRendererKey,
|
||||||
|
} from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { BaseNode } from '../components/base-node'
|
||||||
|
import { BranchAdder } from '../components/branch-adder'
|
||||||
|
import { NodeAdder } from '../components/node-adder';
|
||||||
|
|
||||||
|
/** semi materials */
|
||||||
|
|
||||||
|
export function useEditorProps(
|
||||||
|
initialData: FlowDocumentJSON, // 初始化数据
|
||||||
|
nodeRegistries: FlowNodeRegistry[], // 节点定义
|
||||||
|
): FixedLayoutProps {
|
||||||
|
return useMemo<FixedLayoutProps>(
|
||||||
|
() => ({
|
||||||
|
/**
|
||||||
|
* Whether to enable the background
|
||||||
|
*/
|
||||||
|
background: true,
|
||||||
|
/**
|
||||||
|
* Whether it is read-only or not, the node cannot be dragged in read-only mode
|
||||||
|
*/
|
||||||
|
readonly: false,
|
||||||
|
/**
|
||||||
|
* Initial data
|
||||||
|
* 初始化数据
|
||||||
|
*/
|
||||||
|
initialData,
|
||||||
|
/**
|
||||||
|
* 画布节点定义
|
||||||
|
*/
|
||||||
|
nodeRegistries,
|
||||||
|
/**
|
||||||
|
* Get the default node registry, which will be merged with the 'nodeRegistries'
|
||||||
|
* 提供默认的节点注册,这个会和 nodeRegistries 做合并
|
||||||
|
*/
|
||||||
|
getNodeDefaultRegistry(type) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
meta: {
|
||||||
|
defaultExpanded: true,
|
||||||
|
},
|
||||||
|
formMeta: {
|
||||||
|
/**
|
||||||
|
* Render form
|
||||||
|
*/
|
||||||
|
render: () => <>
|
||||||
|
<Field<string> name="title">
|
||||||
|
{({ field }) => <div className="demo-fixed-node-title">{field.value}</div>}
|
||||||
|
</Field>
|
||||||
|
<div className="demo-fixed-node-content">
|
||||||
|
<Field<string> name="content">
|
||||||
|
<input />
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Materials, components can be customized based on the key
|
||||||
|
* @see https://github.com/coze-dev/flowgram.ai/blob/main/packages/materials/fixed-semi-materials/src/components/index.tsx
|
||||||
|
* 可以通过 key 自定义 UI 组件
|
||||||
|
*/
|
||||||
|
materials: {
|
||||||
|
renderNodes: {
|
||||||
|
...defaultFixedSemiMaterials,
|
||||||
|
/**
|
||||||
|
* Components can be customized based on key business-side requirements.
|
||||||
|
* 这里可以根据 key 业务侧定制组件
|
||||||
|
*/
|
||||||
|
[FlowRendererKey.ADDER]: NodeAdder,
|
||||||
|
[FlowRendererKey.BRANCH_ADDER]: BranchAdder,
|
||||||
|
// [FlowRendererKey.DRAG_NODE]: DragNode,
|
||||||
|
},
|
||||||
|
renderDefaultNode: BaseNode, // 节点渲染
|
||||||
|
renderTexts: {
|
||||||
|
[FlowTextKey.LOOP_END_TEXT]: 'loop end',
|
||||||
|
[FlowTextKey.LOOP_TRAVERSE_TEXT]: 'looping',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Node engine enable, you can configure formMeta in the FlowNodeRegistry
|
||||||
|
*/
|
||||||
|
nodeEngine: {
|
||||||
|
enable: true,
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
enable: true,
|
||||||
|
enableChangeNode: true, // Listen Node engine data change
|
||||||
|
onApply(ctx, opt) {
|
||||||
|
// Listen change to trigger auto save
|
||||||
|
// console.log('auto save: ', ctx.document.toJSON(), opt);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 画布初始化
|
||||||
|
*/
|
||||||
|
onInit: ctx => {
|
||||||
|
/**
|
||||||
|
* Data can also be dynamically loaded via fromJSON
|
||||||
|
* 也可以通过 fromJSON 动态加载数据
|
||||||
|
*/
|
||||||
|
// ctx.document.fromJSON(initialData)
|
||||||
|
console.log('---- Playground Init ----');
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 画布销毁
|
||||||
|
*/
|
||||||
|
onDispose: () => {
|
||||||
|
console.log('---- Playground Dispose ----');
|
||||||
|
},
|
||||||
|
plugins: () => [
|
||||||
|
/**
|
||||||
|
* Minimap plugin
|
||||||
|
* 缩略图插件
|
||||||
|
*/
|
||||||
|
createMinimapPlugin({
|
||||||
|
disableLayer: true,
|
||||||
|
enableDisplayAllNodes: true,
|
||||||
|
canvasStyle: {
|
||||||
|
canvasWidth: 182,
|
||||||
|
canvasHeight: 102,
|
||||||
|
canvasPadding: 50,
|
||||||
|
canvasBackground: 'rgba(245, 245, 245, 1)',
|
||||||
|
canvasBorderRadius: 10,
|
||||||
|
viewportBackground: 'rgba(235, 235, 235, 1)',
|
||||||
|
viewportBorderRadius: 4,
|
||||||
|
viewportBorderColor: 'rgba(201, 201, 201, 1)',
|
||||||
|
viewportBorderWidth: 1,
|
||||||
|
viewportBorderDashLength: 2,
|
||||||
|
nodeColor: 'rgba(255, 255, 255, 1)',
|
||||||
|
nodeBorderRadius: 2,
|
||||||
|
nodeBorderWidth: 0.145,
|
||||||
|
nodeBorderColor: 'rgba(6, 7, 9, 0.10)',
|
||||||
|
overlayColor: 'rgba(255, 255, 255, 0)',
|
||||||
|
},
|
||||||
|
inactiveDebounceTime: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
60
apps/demo-fixed-layout-simple/src/index.css
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
.demo-fixed-node {
|
||||||
|
align-items: flex-start;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgba(6, 7, 9, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
width: 360px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-fixed-node-title {
|
||||||
|
background-color: #93bfe2;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
padding: 4px 12px;
|
||||||
|
}
|
||||||
|
.demo-fixed-node-content {
|
||||||
|
padding: 16px;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-fixed-adder {
|
||||||
|
width: 28px;
|
||||||
|
height: 18px;
|
||||||
|
background: rgb(187, 191, 196);
|
||||||
|
display: flex;
|
||||||
|
border-radius: 9px;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-fixed-adder.activated {
|
||||||
|
background: #82A7FC
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-fixed-adder.isHorizontal {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.gedit-playground * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
1
apps/demo-fixed-layout-simple/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Editor as DemoFixedLayout } from './editor';
|
||||||
66
apps/demo-fixed-layout-simple/src/initial-data.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置流程数据,数据为 blocks 嵌套的格式
|
||||||
|
*/
|
||||||
|
export const initialData: FlowDocumentJSON = {
|
||||||
|
nodes: [
|
||||||
|
// 开始节点
|
||||||
|
{
|
||||||
|
id: 'start_0',
|
||||||
|
type: 'start',
|
||||||
|
data: {
|
||||||
|
title: 'Start',
|
||||||
|
content: 'start content'
|
||||||
|
},
|
||||||
|
blocks: [],
|
||||||
|
},
|
||||||
|
// 分支节点
|
||||||
|
{
|
||||||
|
id: 'condition_0',
|
||||||
|
type: 'condition',
|
||||||
|
data: {
|
||||||
|
title: 'Condition'
|
||||||
|
},
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: 'branch_0',
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'Branch 0',
|
||||||
|
content: 'branch 1 content'
|
||||||
|
},
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: 'custom_0',
|
||||||
|
type: 'custom',
|
||||||
|
data: {
|
||||||
|
title: 'Custom',
|
||||||
|
content: 'custrom content'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'branch_1',
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'Branch 1',
|
||||||
|
content: 'branch 1 content'
|
||||||
|
},
|
||||||
|
blocks: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 结束节点
|
||||||
|
{
|
||||||
|
id: 'end_0',
|
||||||
|
type: 'end',
|
||||||
|
data: {
|
||||||
|
title: 'End',
|
||||||
|
content: 'end content'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
75
apps/demo-fixed-layout-simple/src/node-registries.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义节点注册
|
||||||
|
*/
|
||||||
|
export const nodeRegistries: FlowNodeRegistry[] = [
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 自定义节点类型
|
||||||
|
*/
|
||||||
|
type: 'condition',
|
||||||
|
/**
|
||||||
|
* 自定义节点扩展:
|
||||||
|
* - loop: 扩展为循环节点
|
||||||
|
* - start: 扩展为开始节点
|
||||||
|
* - dynamicSplit: 扩展为分支节点
|
||||||
|
* - end: 扩展为结束节点
|
||||||
|
* - tryCatch: 扩展为 tryCatch 节点
|
||||||
|
* - default: 扩展为普通节点 (默认)
|
||||||
|
*/
|
||||||
|
extend: 'dynamicSplit',
|
||||||
|
/**
|
||||||
|
* 节点配置信息
|
||||||
|
*/
|
||||||
|
meta: {
|
||||||
|
// isStart: false, // 是否为开始节点
|
||||||
|
// isNodeEnd: false, // 是否为结束节点,结束节点后边无法再添加节点
|
||||||
|
// draggable: false, // 是否可拖拽,如开始节点和结束节点无法拖拽
|
||||||
|
// selectable: false, // 触发器等开始节点不能被框选
|
||||||
|
// deleteDisable: true, // 禁止删除
|
||||||
|
// copyDisable: true, // 禁止copy
|
||||||
|
// addDisable: true, // 禁止添加
|
||||||
|
},
|
||||||
|
onAdd() {
|
||||||
|
return {
|
||||||
|
id: `condition_${nanoid(5)}`,
|
||||||
|
type: 'condition',
|
||||||
|
data: {
|
||||||
|
title: 'Condition',
|
||||||
|
},
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: nanoid(5),
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'If_0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(5),
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'If_1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'custom',
|
||||||
|
meta: {},
|
||||||
|
onAdd() {
|
||||||
|
return {
|
||||||
|
id: `custom_${nanoid(5)}`,
|
||||||
|
type: 'custom',
|
||||||
|
data: {
|
||||||
|
title: 'Custom',
|
||||||
|
content: 'this is custom content'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
21
apps/demo-fixed-layout-simple/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "esnext",
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["es6", "dom", "es2020", "es2019.Array"]
|
||||||
|
},
|
||||||
|
"include": ["./src"],
|
||||||
|
}
|
||||||
74
apps/demo-fixed-layout/.eslintrc.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
requireConfigFile: false,
|
||||||
|
babelOptions: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
cwd: __dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/__mocks__',
|
||||||
|
'**/node_modules',
|
||||||
|
'**/build',
|
||||||
|
'**/dist',
|
||||||
|
'**/es',
|
||||||
|
'**/lib',
|
||||||
|
'**/.codebase',
|
||||||
|
'**/.changeset',
|
||||||
|
'**/config',
|
||||||
|
'**/common/scripts',
|
||||||
|
'**/output',
|
||||||
|
'error-log-str.js',
|
||||||
|
'*.bundle.js',
|
||||||
|
'*.min.js',
|
||||||
|
'*.js.map',
|
||||||
|
'**/output',
|
||||||
|
'**/*.log',
|
||||||
|
'**/tsconfig.tsbuildinfo',
|
||||||
|
'**/vitest.config.ts',
|
||||||
|
'package.json',
|
||||||
|
'*.json',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
'react/no-deprecated': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'lines-between-class-members': 'warn',
|
||||||
|
'react/jsx-no-useless-fragment': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
'no-empty-fuNction': 'off',
|
||||||
|
'prefer-destructurin': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'no-empty-function': 'off',
|
||||||
|
'no-multi-assign': 'off',
|
||||||
|
'arrow-body-style': 'warn',
|
||||||
|
'no-useless-constructor': 'off',
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
'max-classes-per-file': 'off',
|
||||||
|
'grouped-accessor-pairs': 'off',
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-restricted-syntax': 'off',
|
||||||
|
'react/destructuring-assignment': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'consistent-return': 'off',
|
||||||
|
'jsx-a11y/no-static-element-interactions': 'off',
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
'no-bitwise': 'off',
|
||||||
|
'no-case-declarations': 'off',
|
||||||
|
'react/no-array-index-key': 'off',
|
||||||
|
'react/require-default-props': 'off',
|
||||||
|
'no-dupe-class-members': 'off',
|
||||||
|
'react/jsx-props-no-spreading': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
'default-param-last': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
},
|
||||||
|
}
|
||||||
12
apps/demo-fixed-layout/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bundler="rspack">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Flow FixedLayoutEditor Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
59
apps/demo-fixed-layout/package.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "@flowgram.ai/demo-fixed-layout",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"files": [
|
||||||
|
"src/",
|
||||||
|
".eslintrc.js",
|
||||||
|
".gitignore",
|
||||||
|
"index.html",
|
||||||
|
"package.json",
|
||||||
|
"rspack.config.js",
|
||||||
|
"tsconfig.json"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "exit 0",
|
||||||
|
"build:fast": "exit 0",
|
||||||
|
"build:watch": "exit 0",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"dev": "MODE=app NODE_ENV=development rspack serve",
|
||||||
|
"lint": "eslint ./src --cache",
|
||||||
|
"lint:fix": "eslint ./src --fix",
|
||||||
|
"start": "NODE_ENV=development rspack serve",
|
||||||
|
"test": "exit",
|
||||||
|
"test:cov": "exit",
|
||||||
|
"watch": "exit 0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@douyinfe/semi-icons": "^2.72.3",
|
||||||
|
"@douyinfe/semi-ui": "^2.72.3",
|
||||||
|
"@flowgram.ai/fixed-layout-editor": "workspace:*",
|
||||||
|
"@flowgram.ai/fixed-semi-materials": "workspace:*",
|
||||||
|
"@flowgram.ai/group-plugin": "workspace:*",
|
||||||
|
"@flowgram.ai/minimap-plugin": "workspace:*",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"nanoid": "^4.0.2",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"styled-components": "^5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rspack/cli": "0.2.1",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/node": "^18",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"@types/styled-components": "^5",
|
||||||
|
"@typescript-eslint/parser": "^6.10.0",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"less": "^4.1.2",
|
||||||
|
"less-loader": "^6"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
}
|
||||||
|
}
|
||||||
64
apps/demo-fixed-layout/rspack.config.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const isCI = process.env.CI === 'true';
|
||||||
|
const isSCM = !!process.env.BUILD_BRANCH;
|
||||||
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
/**
|
||||||
|
* @type {import('@rspack/cli').Configuration}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
mode: process.env.NODE_ENV,
|
||||||
|
context: __dirname,
|
||||||
|
target: ['web'],
|
||||||
|
entry: {
|
||||||
|
main: './src/app.tsx',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
react: require.resolve('react'),
|
||||||
|
'react-dom': require.resolve('react-dom'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
builtins: {
|
||||||
|
// https://www.rspack.dev/config/builtins.html#builtinshtml
|
||||||
|
html: [
|
||||||
|
{
|
||||||
|
template: './index.html',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
progress: !isSCM ? {} : false,
|
||||||
|
treeShaking: isProd,
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
// https://www.rspack.dev/config/module.html#rule
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(png|gif|jpg|jpeg|svg|woff2)$/,
|
||||||
|
type: 'asset',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(less|css)$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'less-loader',
|
||||||
|
options: {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'css',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
/** module is too large now, we may need better way to tackle this in the future */
|
||||||
|
stats: isCI
|
||||||
|
? { all: false, modules: true, assets: true, chunks: true, warnings: true, errors: true }
|
||||||
|
: {
|
||||||
|
modules: false,
|
||||||
|
all: false,
|
||||||
|
warnings: false,
|
||||||
|
errors: true,
|
||||||
|
timings: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
7
apps/demo-fixed-layout/src/app.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { Editor } from './editor';
|
||||||
|
|
||||||
|
const app = createRoot(document.getElementById('root')!);
|
||||||
|
|
||||||
|
app.render(<Editor />);
|
||||||
9
apps/demo-fixed-layout/src/assets/icon-condition.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="44" height="45" viewBox="0 0 44 45" fill="none" class="injected-svg" data-src="https://lf3-static.bytednsdoc.com/obj/eden-cn/uvpahtvabh_lm_zhhwh/ljhwZthlaukjlkulzlp/activity_icons/exclusive-split-0518.svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4705 14.0152C15.299 12.8436 15.299 10.944 16.4705 9.77244L20.7131 5.5297C21.8846 4.3581 23.784 4.3581 24.9556 5.5297L29.1981 9.77244C30.3697 10.944 30.3697 12.8436 29.1981 14.0152L25.1206 18.0929H32.6674C36.5334 18.0929 39.6674 21.2269 39.6674 25.0929V33.154V33.3271V37.154C39.6674 38.2585 38.7719 39.154 37.6674 39.154H33.6674C32.5628 39.154 31.6674 38.2585 31.6674 37.154V33.3271V33.154V26.0929H23.5948H15.6674V33.1327L17.2685 33.1244C18.8397 33.1163 19.6322 35.0156 18.5212 36.1266L12.7374 41.9103C12.0506 42.5971 10.9371 42.5971 10.2503 41.9103L4.52588 36.1859C3.42107 35.0811 4.19797 33.1917 5.76038 33.1837L7.66737 33.1739V25.0929C7.66737 21.227 10.8014 18.0929 14.6674 18.0929H20.5481L16.4705 14.0152Z" fill="url(#paint0_linear_2752_183702-7)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_2752_183702-7" x1="38.52" y1="43.3915" x2="8.09686" y2="4.6982" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#3370FF"/>
|
||||||
|
<stop offset="0.997908" stop-color="#33A9FF"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
apps/demo-fixed-layout/src/assets/icon-end.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
apps/demo-fixed-layout/src/assets/icon-if.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
apps/demo-fixed-layout/src/assets/icon-llm.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
9
apps/demo-fixed-layout/src/assets/icon-loop.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 44 44" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1112 5.9556C31.0632 4.90759 29.2716 5.65513 29.2792 7.13722L29.2873 8.71503H12.1184C8.37321 8.71503 5.33711 11.7511 5.33711 15.4963H5.34238C5.33971 15.538 5.33839 15.58 5.33846 15.6224L5.34892 21.6473C5.35171 23.2499 7.22508 24.1194 8.4509 23.087L12.2732 19.8679C12.9121 19.3298 13.2806 18.5369 13.2801 17.7016L13.2795 16.715H29.3285L29.3351 17.9931C29.3427 19.4669 31.125 20.1998 32.1671 19.1576L37.5671 13.7576C38.215 13.1098 38.215 12.0594 37.5671 11.4115L32.1112 5.9556ZM13.279 15.8694L13.2788 15.6243C13.2788 15.5813 13.2773 15.5386 13.2745 15.4963H13.3371C13.3371 15.6265 13.3167 15.7518 13.279 15.8694ZM11.4759 37.9731C12.5239 39.0211 14.3156 38.2736 14.3079 36.7915L14.2998 35.2137H31.4687C35.2139 35.2137 38.25 32.1776 38.25 28.4324H38.2447C38.2474 28.3907 38.2487 28.3487 38.2487 28.3063L38.2382 22.2814C38.2354 20.6788 36.362 19.8093 35.1362 20.8417L31.314 24.0608C30.675 24.599 30.3065 25.3918 30.307 26.2272L30.3076 27.2137H14.2586L14.252 25.9356C14.2444 24.4618 12.4622 23.7289 11.42 24.7711L6.02002 30.1711C5.37215 30.819 5.37215 31.8694 6.02002 32.5172L11.4759 37.9731ZM30.3082 28.0593L30.3083 28.3044C30.3083 28.3474 30.3098 28.3901 30.3127 28.4324H30.25C30.25 28.3023 30.2704 28.1769 30.3082 28.0593Z" fill="url(#paint0_linear_775_1137)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_775_1137" x1="6.39609" y1="39.3063" x2="32.5905" y2="4.10488" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#3370FF"/>
|
||||||
|
<stop offset="0.997908" stop-color="#33A9FF"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
13
apps/demo-fixed-layout/src/assets/icon-mouse.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export function MouseIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="34" height="52" viewBox="0 0 34 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M30.9998 16.6666V35.3333C30.9998 37.5748 30.9948 38.4695 30.9 39.1895C30.2108 44.4247 26.0912 48.5443 20.856 49.2335C20.1361 49.3283 19.2413 49.3333 16.9998 49.3333C14.7584 49.3333 13.8636 49.3283 13.1437 49.2335C7.90847 48.5443 3.78888 44.4247 3.09965 39.1895C3.00487 38.4695 2.99984 37.5748 2.99984 35.3333V16.6666C2.99984 14.4252 3.00487 13.5304 3.09965 12.8105C3.78888 7.57528 7.90847 3.45569 13.1437 2.76646C13.7232 2.69017 14.4159 2.67202 15.8332 2.66785V9.86573C14.4738 10.3462 13.4998 11.6426 13.4998 13.1666V17.8332C13.4998 19.3571 14.4738 20.6536 15.8332 21.1341V23.6666C15.8332 24.3109 16.3555 24.8333 16.9998 24.8333C17.6442 24.8333 18.1665 24.3109 18.1665 23.6666V21.1341C19.5259 20.6536 20.4998 19.3572 20.4998 17.8332V13.1666C20.4998 11.6426 19.5259 10.3462 18.1665 9.86571V2.66785C19.5837 2.67202 20.2765 2.69017 20.856 2.76646C26.0912 3.45569 30.2108 7.57528 30.9 12.8105C30.9948 13.5304 30.9998 14.4252 30.9998 16.6666ZM0.666504 16.6666C0.666504 14.4993 0.666504 13.4157 0.786276 12.5059C1.61335 6.22368 6.55687 1.28016 12.8391 0.453085C13.7489 0.333313 14.8325 0.333313 16.9998 0.333313C19.1671 0.333313 20.2508 0.333313 21.1605 0.453085C27.4428 1.28016 32.3863 6.22368 33.2134 12.5059C33.3332 13.4157 33.3332 14.4994 33.3332 16.6666V35.3333C33.3332 37.5006 33.3332 38.5843 33.2134 39.494C32.3863 45.7763 27.4428 50.7198 21.1605 51.5469C20.2508 51.6666 19.1671 51.6666 16.9998 51.6666C14.8325 51.6666 13.7489 51.6666 12.8391 51.5469C6.55687 50.7198 1.61335 45.7763 0.786276 39.494C0.666504 38.5843 0.666504 37.5006 0.666504 35.3333V16.6666ZM15.8332 13.1666C15.8332 13.0011 15.8676 12.8437 15.9297 12.7011C15.9886 12.566 16.0722 12.4443 16.1749 12.3416C16.386 12.1305 16.6777 11.9999 16.9998 11.9999C17.6435 11.9999 18.1654 12.5212 18.1665 13.1646L18.1665 13.1666V17.8332L18.1665 17.8353C18.1665 17.8364 18.1665 17.8376 18.1665 17.8387C18.1661 17.9132 18.1588 17.986 18.1452 18.0565C18.0853 18.3656 17.9033 18.6312 17.6515 18.8011C17.4655 18.9266 17.2412 18.9999 16.9998 18.9999C16.3555 18.9999 15.8332 18.4776 15.8332 17.8332V13.1666Z"
|
||||||
|
fill="currentColor"
|
||||||
|
fillOpacity="0.8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
apps/demo-fixed-layout/src/assets/icon-pad.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export function PadIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="48" height="38" viewBox="0 0 48 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect
|
||||||
|
x="1.83317"
|
||||||
|
y="1.49998"
|
||||||
|
width="44.3333"
|
||||||
|
height="35"
|
||||||
|
rx="3.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.8"
|
||||||
|
strokeWidth="2.33333"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.6665 30.6667H33.3332"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.8"
|
||||||
|
strokeWidth="2.33333"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
apps/demo-fixed-layout/src/assets/icon-parallel.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg t="1724931640169" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1762"
|
||||||
|
width="20" height="20">
|
||||||
|
<path
|
||||||
|
d="M1024 800a96 96 0 0 1-96 96h-192a96 96 0 0 1-96-96v-64a96 96 0 0 1 96-96h62.656c-2.56-44.416-11.84-70.72-24.576-79.36-15.36-10.304-32.896-12.928-79.552-13.632l-17.28-0.32a436.544 436.544 0 0 1-24.32-1.152l-14.72-1.472a185.792 185.792 0 0 1-75.712-24.832c-19.968-12.032-36.608-33.92-50.944-65.92-14.272 32-30.912 53.888-50.88 65.92a185.792 185.792 0 0 1-75.648 24.832l-14.72 1.472c-7.936 0.64-14.72 0.96-24.32 1.152l-17.28 0.32c-46.72 0.64-64.256 3.328-79.616 13.696-12.736 8.576-22.016 34.88-24.512 79.296H288A96 96 0 0 1 384 736v64A96 96 0 0 1 288 896h-192A96 96 0 0 1 0 800v-64A96 96 0 0 1 96 640h64.448c3.2-65.664 19.52-109.888 52.864-132.352 24.96-16.896 47.04-22.208 89.28-23.936l47.36-1.152c3.84-0.128 7.168-0.256 10.496-0.512l4.992-0.32c25.984-1.984 45.312-7.04 62.144-17.28 12.8-7.68 27.392-34.752 41.152-80.32L416 384A96 96 0 0 1 320 288v-64A96 96 0 0 1 416 128h192A96 96 0 0 1 704 224v64A96 96 0 0 1 608 384l-53.504 0.128c13.696 45.568 28.352 72.64 41.088 80.32 16.832 10.24 36.16 15.296 62.208 17.28l4.992 0.32c3.264 0.256 6.592 0.384 10.432 0.512l47.36 1.152c42.24 1.728 64.32 7.04 89.344 23.936 33.28 22.4 49.6 66.688 52.8 132.352h65.28a96 96 0 0 1 96 96z m-704 0v-64a32 32 0 0 0-32-32h-192a32 32 0 0 0-32 32v64a32 32 0 0 0 32 32h192a32 32 0 0 0 32-32z m320-512v-64a32 32 0 0 0-32-32h-192a32 32 0 0 0-32 32v64a32 32 0 0 0 32 32h192a32 32 0 0 0 32-32z m320 512v-64a32 32 0 0 0-32-32h-192a32 32 0 0 0-32 32v64a32 32 0 0 0 32 32h192a32 32 0 0 0 32-32z"
|
||||||
|
fill="#666666" p-id="1763"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
apps/demo-fixed-layout/src/assets/icon-start.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
42
apps/demo-fixed-layout/src/components/base-node/index.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { ConfigProvider } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { NodeRenderContext } from '../../context';
|
||||||
|
import { BaseNodeStyle, ErrorIcon } from './styles';
|
||||||
|
|
||||||
|
export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
|
||||||
|
/**
|
||||||
|
* Provides methods related to node rendering
|
||||||
|
* 提供节点渲染相关的方法
|
||||||
|
*/
|
||||||
|
const nodeRender = useNodeRender();
|
||||||
|
/**
|
||||||
|
* It can only be used when nodeEngine is enabled
|
||||||
|
* 只有在节点引擎开启时候才能使用表单
|
||||||
|
*/
|
||||||
|
const form = nodeRender.form;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to make the Tooltip scale with the node, which can be implemented by itself depending on the UI library
|
||||||
|
* 用于让 Tooltip 跟随节点缩放, 这个可以根据不同的 ui 库自己实现
|
||||||
|
*/
|
||||||
|
const getPopupContainer = useCallback(() => node.renderData.node || document.body, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider getPopupContainer={getPopupContainer}>
|
||||||
|
{form?.state.invalid && <ErrorIcon />}
|
||||||
|
<BaseNodeStyle
|
||||||
|
onMouseEnter={nodeRender.onMouseEnter}
|
||||||
|
onMouseLeave={nodeRender.onMouseLeave}
|
||||||
|
style={{
|
||||||
|
...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),
|
||||||
|
outline: form?.state.invalid ? '1px solid red' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NodeRenderContext.Provider value={nodeRender}>{form?.render()}</NodeRenderContext.Provider>
|
||||||
|
</BaseNodeStyle>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
31
apps/demo-fixed-layout/src/components/base-node/styles.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { IconInfoCircle } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
export const BaseNodeStyle = styled.div`
|
||||||
|
align-items: flex-start;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgba(6, 7, 9, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
width: 360px;
|
||||||
|
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ErrorIcon = () => (
|
||||||
|
<IconInfoCircle
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
color: 'red',
|
||||||
|
left: -6,
|
||||||
|
top: -6,
|
||||||
|
zIndex: 1,
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
49
apps/demo-fixed-layout/src/components/branch-adder/index.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { type FlowNodeEntity, useClientContext } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { IconPlus } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { BlockNodeRegistry } from '../../nodes/block';
|
||||||
|
import { Container } from './styles';
|
||||||
|
|
||||||
|
interface PropsType {
|
||||||
|
activated?: boolean;
|
||||||
|
node: FlowNodeEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BranchAdder(props: PropsType) {
|
||||||
|
const { activated, node } = props;
|
||||||
|
const nodeData = node.firstChild!.renderData;
|
||||||
|
const ctx = useClientContext();
|
||||||
|
const { operation, playground } = ctx;
|
||||||
|
const { isVertical } = node;
|
||||||
|
|
||||||
|
function addBranch() {
|
||||||
|
const block = operation.addBlock(node, BlockNodeRegistry.onAdd!(ctx, node));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
playground.scrollToView({
|
||||||
|
bounds: block.bounds,
|
||||||
|
scrollToCenter: true,
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
if (playground.config.readonlyOrDisabled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
isVertical={isVertical}
|
||||||
|
activated={activated}
|
||||||
|
onMouseEnter={() => nodeData?.toggleMouseEnter()}
|
||||||
|
onMouseLeave={() => nodeData?.toggleMouseLeave()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
addBranch();
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ flexGrow: 1, textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
<IconPlus />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Container = styled.div<{ activated?: boolean; isVertical: boolean }>`
|
||||||
|
width: 28px;
|
||||||
|
height: 18px;
|
||||||
|
background: ${props => (props.activated ? '#82A7FC' : 'rgb(187, 191, 196)')};
|
||||||
|
display: flex;
|
||||||
|
border-radius: 9px;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
transform: ${props => (props.isVertical ? '' : 'rotate(90deg)')};
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
47
apps/demo-fixed-layout/src/components/drag-node/index.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import type { FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { FlowNodeRegistries } from '../../nodes';
|
||||||
|
import { Icon } from '../../form-components/form-header/styles';
|
||||||
|
import { UIDragNodeContainer, UIDragCounts } from './styles';
|
||||||
|
|
||||||
|
export interface PropsType {
|
||||||
|
dragStart: FlowNodeEntity;
|
||||||
|
dragNodes: FlowNodeEntity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DragNode(props: PropsType): JSX.Element {
|
||||||
|
const { dragStart, dragNodes } = props;
|
||||||
|
|
||||||
|
const icon = FlowNodeRegistries.find(registry => registry.type === dragStart?.flowNodeType)?.info
|
||||||
|
?.icon;
|
||||||
|
|
||||||
|
const dragLength = (dragNodes || [])
|
||||||
|
.map(_node =>
|
||||||
|
_node.allCollapsedChildren.length
|
||||||
|
? _node.allCollapsedChildren.filter(_n => !_n.hidden).length
|
||||||
|
: 1,
|
||||||
|
)
|
||||||
|
.reduce((acm, curr) => acm + curr, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIDragNodeContainer>
|
||||||
|
<Icon src={icon} />
|
||||||
|
{dragStart?.id}
|
||||||
|
{dragLength > 1 && (
|
||||||
|
<>
|
||||||
|
<UIDragCounts>{dragLength}</UIDragCounts>
|
||||||
|
<UIDragNodeContainer
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 5,
|
||||||
|
right: -5,
|
||||||
|
left: 5,
|
||||||
|
bottom: -5,
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</UIDragNodeContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
apps/demo-fixed-layout/src/components/drag-node/styles.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const primary = 'hsl(252 62% 54.9%)';
|
||||||
|
const primaryOpacity09 = 'hsl(252deg 62% 55% / 9%)';
|
||||||
|
|
||||||
|
export const UIDragNodeContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 19px;
|
||||||
|
border: 1px solid ${primary};
|
||||||
|
padding: 0 15px;
|
||||||
|
&:hover: {
|
||||||
|
background-color: ${primaryOpacity09};
|
||||||
|
color: ${primary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UIDragCounts = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
background-color: ${primary};
|
||||||
|
`;
|
||||||
2
apps/demo-fixed-layout/src/components/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { DemoTools } from './tools';
|
||||||
|
export { DragNode } from './drag-node';
|
||||||
162
apps/demo-fixed-layout/src/components/node-adder/index.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { useClientContext } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { type FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { Popover, Toast, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { IconCopyAdd, IconPlusCircle } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { NodeList } from '../node-list';
|
||||||
|
import { readData } from '../../shortcuts/utils';
|
||||||
|
import { generateNodeId } from './utils';
|
||||||
|
import { PasteIcon, Wrap } from './styles';
|
||||||
|
|
||||||
|
const generateNewIdForChildren = (n: FlowNodeEntity): FlowNodeEntity => {
|
||||||
|
if (n.blocks) {
|
||||||
|
return {
|
||||||
|
...n,
|
||||||
|
id: generateNodeId(n),
|
||||||
|
blocks: n.blocks.map(b => generateNewIdForChildren(b)),
|
||||||
|
} as FlowNodeEntity;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...n,
|
||||||
|
id: generateNodeId(n),
|
||||||
|
} as FlowNodeEntity;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Adder(props: {
|
||||||
|
from: FlowNodeEntity;
|
||||||
|
to?: FlowNodeEntity;
|
||||||
|
hoverActivated: boolean;
|
||||||
|
}) {
|
||||||
|
const { from } = props;
|
||||||
|
const isVertical = from.isVertical;
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const { playground, operation, clipboard } = useClientContext();
|
||||||
|
|
||||||
|
const [pasteIconVisible, setPasteIconVisible] = useState(false);
|
||||||
|
|
||||||
|
const activated = useMemo(
|
||||||
|
() => props.hoverActivated && !playground.config.readonly,
|
||||||
|
[props.hoverActivated, playground.config.readonly],
|
||||||
|
);
|
||||||
|
|
||||||
|
const add = (addProps: any) => {
|
||||||
|
const blocks = addProps.blocks ? addProps.blocks : undefined;
|
||||||
|
const block = operation.addFromNode(from, {
|
||||||
|
...addProps,
|
||||||
|
blocks,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
playground.scrollToView({
|
||||||
|
bounds: block.bounds,
|
||||||
|
scrollToCenter: true,
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaste = useCallback(async (e: any) => {
|
||||||
|
try {
|
||||||
|
e.stopPropagation();
|
||||||
|
const nodes = await readData(clipboard);
|
||||||
|
|
||||||
|
if (!nodes) {
|
||||||
|
Toast.error({
|
||||||
|
content: 'The clipboard content has been updated, please copy the node again.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.reverse().forEach((n: FlowNodeEntity) => {
|
||||||
|
const newNodeData = generateNewIdForChildren(n);
|
||||||
|
operation.addFromNode(from, newNodeData);
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: 'Paste successfully!',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
Toast.error({
|
||||||
|
content: (
|
||||||
|
<Typography.Text>
|
||||||
|
Paste failed, please check if you have permission to read the clipboard,
|
||||||
|
</Typography.Text>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
if (playground.config.readonly) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={setVisible}
|
||||||
|
content={<NodeList onSelect={add} from={from} />}
|
||||||
|
placement="right"
|
||||||
|
trigger="click"
|
||||||
|
popupAlign={{ offset: [30, 0] }}
|
||||||
|
overlayStyle={{
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Wrap
|
||||||
|
style={
|
||||||
|
props.hoverActivated
|
||||||
|
? {
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{props.hoverActivated ? (
|
||||||
|
<IconPlusCircle
|
||||||
|
onClick={() => {
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
const data = clipboard.readText();
|
||||||
|
setPasteIconVisible(!!data);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: '#3370ff',
|
||||||
|
borderRadius: 15,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
{activated && pasteIconVisible && (
|
||||||
|
<Popover position="top" showArrow content="Paste">
|
||||||
|
<PasteIcon
|
||||||
|
onClick={handlePaste}
|
||||||
|
style={
|
||||||
|
isVertical
|
||||||
|
? {
|
||||||
|
right: -25,
|
||||||
|
top: 0,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
right: 0,
|
||||||
|
top: -20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconCopyAdd
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--semi-color-bg-0)',
|
||||||
|
borderRadius: 15,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PasteIcon>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</Wrap>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
apps/demo-fixed-layout/src/components/node-adder/styles.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const PasteIcon = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
color: #3370ff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Wrap = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: rgb(143, 149, 158);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
svg: {
|
||||||
|
transform: scale(0.7);
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
export const generateNodeId = (n: FlowNodeEntity) => `${n.type || n.flowNodeType}_${nanoid()}`;
|
||||||
69
apps/demo-fixed-layout/src/components/node-list.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
FlowNodeEntity,
|
||||||
|
FlowNodeRegistry,
|
||||||
|
useClientContext,
|
||||||
|
} from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { FlowNodeRegistries } from '../nodes';
|
||||||
|
|
||||||
|
const NodeWrap = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 19px;
|
||||||
|
padding: 0 15px;
|
||||||
|
&:hover {
|
||||||
|
background-color: hsl(252deg 62% 55% / 9%);
|
||||||
|
color: hsl(252 62% 54.9%);
|
||||||
|
},
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NodeLabel = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function Node(props: { label: string; icon: JSX.Element; onClick: () => void; disabled: boolean }) {
|
||||||
|
return (
|
||||||
|
<NodeWrap
|
||||||
|
onClick={props.disabled ? undefined : props.onClick}
|
||||||
|
style={props.disabled ? { opacity: 0.3 } : {}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 14 }}>{props.icon}</div>
|
||||||
|
<NodeLabel>{props.label}</NodeLabel>
|
||||||
|
</NodeWrap>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodesWrap = styled.div`
|
||||||
|
max-height: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function NodeList(props: { onSelect: (meta: any) => void; from: FlowNodeEntity }) {
|
||||||
|
const context = useClientContext();
|
||||||
|
const handleClick = (registry: FlowNodeRegistry) => {
|
||||||
|
const addProps = registry.onAdd(context, props.from);
|
||||||
|
props.onSelect?.(addProps);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<NodesWrap style={{ width: 80 * 2 + 20 }}>
|
||||||
|
{FlowNodeRegistries.map(registry => (
|
||||||
|
<Node
|
||||||
|
key={registry.type}
|
||||||
|
disabled={!(registry.canAdd?.(context, props.from) ?? true)}
|
||||||
|
icon={<img style={{ width: 10, height: 10, borderRadius: 4 }} src={registry.info.icon} />}
|
||||||
|
label={registry.type as string}
|
||||||
|
onClick={() => handleClick(registry)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</NodesWrap>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
import { FunctionComponent, useMemo } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useStartDragNode,
|
||||||
|
FlowNodeRenderData,
|
||||||
|
FlowNodeBaseType,
|
||||||
|
FlowGroupService,
|
||||||
|
type FlowNodeEntity,
|
||||||
|
SelectorBoxPopoverProps,
|
||||||
|
} from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { Button, ButtonGroup, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
IconCopy,
|
||||||
|
IconDeleteStroked,
|
||||||
|
IconExpand,
|
||||||
|
IconHandle,
|
||||||
|
IconShrink,
|
||||||
|
} from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { FlowCommandId } from '../../shortcuts/constants';
|
||||||
|
import { IconGroupOutlined } from '../../plugins/group-plugin/icons';
|
||||||
|
|
||||||
|
const BUTTON_HEIGHT = 24;
|
||||||
|
|
||||||
|
export const SelectorBoxPopover: FunctionComponent<SelectorBoxPopoverProps> = ({
|
||||||
|
bounds,
|
||||||
|
children,
|
||||||
|
flowSelectConfig,
|
||||||
|
commandRegistry,
|
||||||
|
}) => {
|
||||||
|
const selectNodes = flowSelectConfig.selectedNodes;
|
||||||
|
|
||||||
|
const { startDrag } = useStartDragNode();
|
||||||
|
|
||||||
|
const draggable = selectNodes[0]?.getData(FlowNodeRenderData)?.draggable;
|
||||||
|
|
||||||
|
// Does the selected component have a group node? (High-cost computation must use memo)
|
||||||
|
const hasGroup: boolean = useMemo(() => {
|
||||||
|
if (!selectNodes || selectNodes.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const findGroupInNodes = (nodes: FlowNodeEntity[]): boolean =>
|
||||||
|
nodes.some(node => {
|
||||||
|
if (node.flowNodeType === FlowNodeBaseType.GROUP) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (node.blocks && node.blocks.length) {
|
||||||
|
return findGroupInNodes(node.blocks);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return findGroupInNodes(selectNodes);
|
||||||
|
}, [selectNodes]);
|
||||||
|
|
||||||
|
const canGroup = !hasGroup && FlowGroupService.validate(selectNodes);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: bounds.right,
|
||||||
|
top: bounds.top,
|
||||||
|
transform: 'translate(-100%, -100%)',
|
||||||
|
}}
|
||||||
|
onMouseDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonGroup
|
||||||
|
size="small"
|
||||||
|
style={{ display: 'flex', flexWrap: 'nowrap', height: BUTTON_HEIGHT }}
|
||||||
|
>
|
||||||
|
{draggable && (
|
||||||
|
<Tooltip content="Drag">
|
||||||
|
<Button
|
||||||
|
style={{ cursor: 'grab', height: BUTTON_HEIGHT }}
|
||||||
|
icon={<IconHandle />}
|
||||||
|
type="primary"
|
||||||
|
theme="solid"
|
||||||
|
onMouseDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startDrag(e, {
|
||||||
|
dragStartEntity: selectNodes[0],
|
||||||
|
dragEntities: selectNodes,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip content={'Collapse'}>
|
||||||
|
<Button
|
||||||
|
icon={<IconShrink />}
|
||||||
|
style={{ height: BUTTON_HEIGHT }}
|
||||||
|
type="primary"
|
||||||
|
theme="solid"
|
||||||
|
onMouseDown={e => {
|
||||||
|
commandRegistry.executeCommand(FlowCommandId.COLLAPSE);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={'Expand'}>
|
||||||
|
<Button
|
||||||
|
icon={<IconExpand />}
|
||||||
|
style={{ height: BUTTON_HEIGHT }}
|
||||||
|
type="primary"
|
||||||
|
theme="solid"
|
||||||
|
onMouseDown={e => {
|
||||||
|
commandRegistry.executeCommand(FlowCommandId.EXPAND);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={'Group'}>
|
||||||
|
<Button
|
||||||
|
icon={<IconGroupOutlined />}
|
||||||
|
type="primary"
|
||||||
|
theme="solid"
|
||||||
|
style={{
|
||||||
|
display: canGroup ? 'inherit' : 'none',
|
||||||
|
height: BUTTON_HEIGHT,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
commandRegistry.executeCommand(FlowCommandId.GROUP);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={'Copy'}>
|
||||||
|
<Button
|
||||||
|
icon={<IconCopy />}
|
||||||
|
style={{ height: BUTTON_HEIGHT }}
|
||||||
|
type="primary"
|
||||||
|
theme="solid"
|
||||||
|
onClick={() => {
|
||||||
|
commandRegistry.executeCommand(FlowCommandId.COPY);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={'Delete'}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
theme="solid"
|
||||||
|
icon={<IconDeleteStroked />}
|
||||||
|
style={{ height: BUTTON_HEIGHT }}
|
||||||
|
onClick={() => {
|
||||||
|
commandRegistry.executeCommand(FlowCommandId.DELETE);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ cursor: draggable ? 'grab' : 'auto' }}
|
||||||
|
onMouseDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startDrag(e, {
|
||||||
|
dragStartEntity: selectNodes[0],
|
||||||
|
dragEntities: selectNodes,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
13
apps/demo-fixed-layout/src/components/tools/fit-view.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { IconButton, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
import { IconExpand } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
export const FitView = (props: { fitView: () => void }) => (
|
||||||
|
<Tooltip content="FitView">
|
||||||
|
<IconButton
|
||||||
|
icon={<IconExpand />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
onClick={() => props.fitView()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
60
apps/demo-fixed-layout/src/components/tools/index.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
usePlayground,
|
||||||
|
usePlaygroundTools,
|
||||||
|
useRefresh,
|
||||||
|
} from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { Tooltip, IconButton } from '@douyinfe/semi-ui';
|
||||||
|
import { IconUndo, IconRedo } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { ZoomSelect } from './zoom-select';
|
||||||
|
import { SwitchVertical } from './switch-vertical';
|
||||||
|
import { ToolContainer, ToolSection } from './styles';
|
||||||
|
import { Save } from './save';
|
||||||
|
import { Readonly } from './readonly';
|
||||||
|
import { MinimapSwitch } from './minimap-switch';
|
||||||
|
import { Minimap } from './minimap';
|
||||||
|
import { FitView } from './fit-view';
|
||||||
|
|
||||||
|
export const DemoTools = () => {
|
||||||
|
const tools = usePlaygroundTools();
|
||||||
|
const [minimapVisible, setMinimapVisible] = useState(false);
|
||||||
|
const playground = usePlayground();
|
||||||
|
const refresh = useRefresh();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh());
|
||||||
|
return () => disposable.dispose();
|
||||||
|
}, [playground]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolContainer className="fixed-demo-tools">
|
||||||
|
<ToolSection>
|
||||||
|
<SwitchVertical />
|
||||||
|
<ZoomSelect />
|
||||||
|
<FitView fitView={tools.fitView} />
|
||||||
|
<MinimapSwitch minimapVisible={minimapVisible} setMinimapVisible={setMinimapVisible} />
|
||||||
|
<Minimap visible={minimapVisible} />
|
||||||
|
<Readonly />
|
||||||
|
<Tooltip content="Undo">
|
||||||
|
<IconButton
|
||||||
|
theme="borderless"
|
||||||
|
icon={<IconUndo />}
|
||||||
|
disabled={!tools.canUndo}
|
||||||
|
onClick={() => tools.undo()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Redo">
|
||||||
|
<IconButton
|
||||||
|
theme="borderless"
|
||||||
|
icon={<IconRedo />}
|
||||||
|
disabled={!tools.canRedo}
|
||||||
|
onClick={() => tools.redo()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Save disabled={playground.config.readonly} />
|
||||||
|
</ToolSection>
|
||||||
|
</ToolContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { Tooltip, IconButton } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { UIIconGridRectangle } from './styles';
|
||||||
|
|
||||||
|
export const MinimapSwitch = (props: {
|
||||||
|
minimapVisible: boolean;
|
||||||
|
setMinimapVisible: (visible: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const { minimapVisible, setMinimapVisible } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="Minimap">
|
||||||
|
<IconButton
|
||||||
|
theme="borderless"
|
||||||
|
icon={<UIIconGridRectangle visible={minimapVisible} />}
|
||||||
|
onClick={() => setMinimapVisible(!minimapVisible)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
33
apps/demo-fixed-layout/src/components/tools/minimap.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { FlowMinimapService, MinimapRender } from '@flowgram.ai/minimap-plugin';
|
||||||
|
import { useService } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { MinimapContainer } from './styles';
|
||||||
|
|
||||||
|
export const Minimap = ({ visible }: { visible?: boolean }) => {
|
||||||
|
const minimapService = useService(FlowMinimapService);
|
||||||
|
if (!visible) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<MinimapContainer>
|
||||||
|
<MinimapRender
|
||||||
|
service={minimapService}
|
||||||
|
panelStyles={{}}
|
||||||
|
containerStyles={{
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
top: 'unset',
|
||||||
|
right: 'unset',
|
||||||
|
bottom: 'unset',
|
||||||
|
left: 'unset',
|
||||||
|
}}
|
||||||
|
inactiveStyle={{
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
translateX: 0,
|
||||||
|
translateY: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MinimapContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
18
apps/demo-fixed-layout/src/components/tools/readonly.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { usePlayground } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { IconButton } from '@douyinfe/semi-ui';
|
||||||
|
import { IconUnlock, IconLock } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
export const Readonly = () => {
|
||||||
|
const playground = usePlayground();
|
||||||
|
const toggleReadonly = useCallback(() => {
|
||||||
|
playground.config.readonly = !playground.config.readonly;
|
||||||
|
}, [playground]);
|
||||||
|
|
||||||
|
return playground.config.readonly ? (
|
||||||
|
<IconButton theme="borderless" type="tertiary" icon={<IconLock />} onClick={toggleReadonly} />
|
||||||
|
) : (
|
||||||
|
<IconButton theme="borderless" type="tertiary" icon={<IconUnlock />} onClick={toggleReadonly} />
|
||||||
|
);
|
||||||
|
};
|
||||||
60
apps/demo-fixed-layout/src/components/tools/save.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useClientContext,
|
||||||
|
getNodeForm,
|
||||||
|
FlowNodeEntity,
|
||||||
|
} from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { Button, Badge } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
export function Save(props: { disabled: boolean }) {
|
||||||
|
const [errorCount, setErrorCount] = useState(0);
|
||||||
|
const clientContext = useClientContext();
|
||||||
|
|
||||||
|
const updateValidateData = useCallback(() => {
|
||||||
|
const allForms = clientContext.document.getAllNodes().map(node => getNodeForm(node));
|
||||||
|
const count = allForms.filter(form => form?.state.invalid).length;
|
||||||
|
setErrorCount(count);
|
||||||
|
}, [clientContext]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate all node and Save
|
||||||
|
*/
|
||||||
|
const onSave = useCallback(async () => {
|
||||||
|
const allForms = clientContext.document.getAllNodes().map(node => getNodeForm(node));
|
||||||
|
await Promise.all(allForms.map(async form => form?.validate()));
|
||||||
|
console.log('>>>>> save data: ', clientContext.document.toJSON());
|
||||||
|
}, [clientContext]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
* Listen single node validate
|
||||||
|
*/
|
||||||
|
const listenSingleNodeValidate = (node: FlowNodeEntity) => {
|
||||||
|
const form = getNodeForm(node);
|
||||||
|
if (form) {
|
||||||
|
const formValidateDispose = form.onValidate(() => updateValidateData());
|
||||||
|
node.onDispose(() => formValidateDispose.dispose());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
clientContext.document.getAllNodes().map(node => listenSingleNodeValidate(node));
|
||||||
|
const dispose = clientContext.document.onNodeCreate(({ node }) =>
|
||||||
|
listenSingleNodeValidate(node),
|
||||||
|
);
|
||||||
|
return () => dispose.dispose();
|
||||||
|
}, [clientContext]);
|
||||||
|
if (errorCount === 0) {
|
||||||
|
return (
|
||||||
|
<Button disabled={props.disabled} onClick={onSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Badge count={errorCount} position="rightTop" type="danger">
|
||||||
|
<Button type="danger" disabled={props.disabled} onClick={onSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
apps/demo-fixed-layout/src/components/tools/styles.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { IconGridRectangle } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
export const ToolContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: left;
|
||||||
|
min-width: 360px;
|
||||||
|
pointer-events: none;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
z-index: 99;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ToolSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgba(68, 83, 130, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.04) 0px 2px 6px 0px, rgba(0, 0, 0, 0.02) 0px 4px 12px 0px;
|
||||||
|
column-gap: 2px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 4px;
|
||||||
|
pointer-events: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SelectZoom = styled.span`
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(68, 83, 130, 0.25);
|
||||||
|
font-size: 12px;
|
||||||
|
width: 40px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MinimapContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60px;
|
||||||
|
width: 182px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UIIconGridRectangle = styled(IconGridRectangle)<{ visible: boolean }>`
|
||||||
|
color: ${props => (props.visible ? undefined : '#060709cc')};
|
||||||
|
`;
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { usePlaygroundTools } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
import { IconServer } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
export const SwitchVertical = () => {
|
||||||
|
const tools = usePlaygroundTools();
|
||||||
|
return (
|
||||||
|
<Tooltip content={!tools.isVertical ? 'Vertical Layout' : 'Horizontal Layout'}>
|
||||||
|
<Button
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
onClick={() => tools.changeLayout()}
|
||||||
|
icon={
|
||||||
|
<IconServer
|
||||||
|
style={{
|
||||||
|
transform: !tools.isVertical ? '' : 'rotate(90deg)',
|
||||||
|
transition: 'transform .3s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
type="tertiary"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
32
apps/demo-fixed-layout/src/components/tools/zoom-select.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { usePlaygroundTools } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { Divider, Dropdown } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { SelectZoom } from './styles';
|
||||||
|
|
||||||
|
export const ZoomSelect = () => {
|
||||||
|
const tools = usePlaygroundTools();
|
||||||
|
const [dropDownVisible, openDropDown] = useState(false);
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
position="top"
|
||||||
|
trigger="custom"
|
||||||
|
visible={dropDownVisible}
|
||||||
|
onClickOutSide={() => openDropDown(false)}
|
||||||
|
render={
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item onClick={() => tools.zoomin()}>Zoomin</Dropdown.Item>
|
||||||
|
<Dropdown.Item onClick={() => tools.zoomout()}>Zoomout</Dropdown.Item>
|
||||||
|
<Divider layout="horizontal" />
|
||||||
|
<Dropdown.Item onClick={() => tools.updateZoom(0.5)}>50%</Dropdown.Item>
|
||||||
|
<Dropdown.Item onClick={() => tools.updateZoom(1)}>100%</Dropdown.Item>
|
||||||
|
<Dropdown.Item onClick={() => tools.updateZoom(1.5)}>150%</Dropdown.Item>
|
||||||
|
<Dropdown.Item onClick={() => tools.updateZoom(2.0)}>200%</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectZoom onClick={() => openDropDown(true)}>{Math.floor(tools.zoom * 100)}%</SelectZoom>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
apps/demo-fixed-layout/src/context/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { NodeRenderContext } from './node-render-context';
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { type NodeRenderReturnType } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
export const NodeRenderContext = React.createContext<NodeRenderReturnType>({} as any);
|
||||||
24
apps/demo-fixed-layout/src/editor.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { EditorRenderer, FixedLayoutEditorProvider } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { FlowNodeRegistries } from './nodes';
|
||||||
|
import { initialData } from './initial-data';
|
||||||
|
import { useEditorProps } from './hooks/use-editor-props';
|
||||||
|
import { DemoTools } from './components';
|
||||||
|
|
||||||
|
import '@flowgram.ai/fixed-layout-editor/index.css';
|
||||||
|
|
||||||
|
export const Editor = () => {
|
||||||
|
/**
|
||||||
|
* Editor Config
|
||||||
|
*/
|
||||||
|
const editorProps = useEditorProps(initialData, FlowNodeRegistries);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="doc-feature-overview">
|
||||||
|
<FixedLayoutEditorProvider {...editorProps}>
|
||||||
|
<EditorRenderer />
|
||||||
|
<DemoTools />
|
||||||
|
</FixedLayoutEditorProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
apps/demo-fixed-layout/src/form-components/feedback.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { FieldError, FieldState, FieldWarning } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
interface StatePanelProps {
|
||||||
|
errors?: FieldState['errors'];
|
||||||
|
warnings?: FieldState['warnings'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Error = styled.span`
|
||||||
|
font-size: 12px;
|
||||||
|
color: red;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Warning = styled.span`
|
||||||
|
font-size: 12px;
|
||||||
|
color: orange;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Feedback = ({ errors, warnings }: StatePanelProps) => {
|
||||||
|
const renderFeedbacks = (fs: FieldError[] | FieldWarning[] | undefined) => {
|
||||||
|
if (!fs) return null;
|
||||||
|
return fs.map(f => <span key={f.name}>{f.message}</span>);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Error>{renderFeedbacks(errors)}</Error>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Warning>{renderFeedbacks(warnings)}</Warning>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
|
import { FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { NodeRenderContext } from '../../context';
|
||||||
|
import { FormTitleDescription, FormWrapper } from './styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param props
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export function FormContent(props: { children?: React.ReactNode }) {
|
||||||
|
const { expanded, node } = useContext(NodeRenderContext);
|
||||||
|
const registry = node.getNodeRegistry<FlowNodeRegistry>();
|
||||||
|
return (
|
||||||
|
<FormWrapper>
|
||||||
|
{expanded ? (
|
||||||
|
<>
|
||||||
|
<FormTitleDescription>{registry.info?.description}</FormTitleDescription>
|
||||||
|
{props.children}
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
</FormWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const FormWrapper = styled.div`
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FormTitleDescription = styled.div`
|
||||||
|
color: var(--semi-color-text-2);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 0px 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: break-spaces;
|
||||||
|
`;
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { Field, FieldRenderProps, useClientContext } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
import { IconButton, Dropdown, Typography, Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';
|
||||||
|
import { IconMore } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { Feedback } from '../feedback';
|
||||||
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
|
import { FlowCommandId } from '../../shortcuts/constants';
|
||||||
|
import { NodeRenderContext } from '../../context';
|
||||||
|
import { getIcon } from './utils';
|
||||||
|
import { Header, Operators, Title } from './styles';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
function DropdownContent() {
|
||||||
|
const { node, deleteNode } = useContext(NodeRenderContext);
|
||||||
|
const clientContext = useClientContext();
|
||||||
|
const registry = node.getNodeRegistry<FlowNodeRegistry>();
|
||||||
|
const handleCopy = () => {
|
||||||
|
clientContext.playground.commandService.executeCommand(FlowCommandId.COPY, node);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item onClick={handleCopy} disabled={registry.meta!.copyDisable === true}>
|
||||||
|
Copy
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={deleteNode}
|
||||||
|
disabled={!!(registry.canDelete?.(clientContext, node) || registry.meta!.deleteDisable)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormHeader() {
|
||||||
|
const { node, expanded, startDrag, toggleExpand, readonly } = useContext(NodeRenderContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Header
|
||||||
|
onMouseDown={e => {
|
||||||
|
// trigger drag node
|
||||||
|
startDrag(e);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getIcon(node)}
|
||||||
|
<Title>
|
||||||
|
<Field name="title">
|
||||||
|
{({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
|
||||||
|
<div style={{ height: 24 }}>
|
||||||
|
<Text ellipsis={{ showTooltip: true }}>{value}</Text>
|
||||||
|
<Feedback errors={fieldState?.errors} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Title>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={expanded ? <IconSmallTriangleDown /> : <IconSmallTriangleLeft />}
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
onClick={toggleExpand}
|
||||||
|
/>
|
||||||
|
{readonly ? undefined : (
|
||||||
|
<Operators>
|
||||||
|
<Dropdown trigger="hover" position="bottomRight" render={<DropdownContent />}>
|
||||||
|
<IconButton
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
icon={<IconMore />}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</Operators>
|
||||||
|
)}
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Header = styled.div`
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
column-gap: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
background: linear-gradient(#f2f2ff 0%, rgba(0, 0, 0, 0.02) 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Title = styled.div`
|
||||||
|
font-size: 20px;
|
||||||
|
flex: 1;
|
||||||
|
width: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Icon = styled.img`
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
scale: 0.8;
|
||||||
|
border-radius: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Operators = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 4px;
|
||||||
|
`;
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { type FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
|
import { Icon } from './styles';
|
||||||
|
|
||||||
|
export const getIcon = (node: FlowNodeEntity) => {
|
||||||
|
const icon = node.getNodeRegistry<FlowNodeRegistry>().info?.icon;
|
||||||
|
if (!icon) return null;
|
||||||
|
return <Icon src={icon} />;
|
||||||
|
};
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { Field } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { FxExpression } from '../fx-expression';
|
||||||
|
import { FormItem } from '../form-item';
|
||||||
|
import { Feedback } from '../feedback';
|
||||||
|
import { JsonSchema } from '../../typings';
|
||||||
|
import { NodeRenderContext } from '../../context';
|
||||||
|
|
||||||
|
export function FormInputs() {
|
||||||
|
const { readonly } = useContext(NodeRenderContext);
|
||||||
|
return (
|
||||||
|
<Field<JsonSchema> name="inputs">
|
||||||
|
{({ field: inputsField }) => {
|
||||||
|
const required = inputsField.value?.required || [];
|
||||||
|
const properties = inputsField.value?.properties;
|
||||||
|
if (!properties) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
const content = Object.keys(properties).map(key => {
|
||||||
|
const property = properties[key];
|
||||||
|
return (
|
||||||
|
<Field key={key} name={`inputsValues.${key}`} defaultValue={property.default}>
|
||||||
|
{({ field, fieldState }) => (
|
||||||
|
<FormItem
|
||||||
|
name={key}
|
||||||
|
type={property.type as string}
|
||||||
|
required={required.includes(key)}
|
||||||
|
>
|
||||||
|
<FxExpression
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
disabled={readonly}
|
||||||
|
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||||
|
/>
|
||||||
|
<Feedback errors={fieldState?.errors} />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return <>{content}</>;
|
||||||
|
}}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// import styled from 'styled-components';
|
||||||
|
|
||||||
|
// TODO
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
.form-item-type-tag {
|
||||||
|
color: inherit;
|
||||||
|
padding: 0 2px;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
vertical-align: middle;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { Typography, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { TypeTag } from '../type-tag';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
interface FormItemProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
required?: boolean;
|
||||||
|
description?: string;
|
||||||
|
labelWidth?: number;
|
||||||
|
}
|
||||||
|
export function FormItem({
|
||||||
|
children,
|
||||||
|
name,
|
||||||
|
required,
|
||||||
|
description,
|
||||||
|
type,
|
||||||
|
labelWidth,
|
||||||
|
}: FormItemProps): JSX.Element {
|
||||||
|
const renderTitle = useCallback(
|
||||||
|
(showTooltip?: boolean) => (
|
||||||
|
<div style={{ width: '0', display: 'flex', flex: '1' }}>
|
||||||
|
<Text style={{ width: '100%' }} ellipsis={{ showTooltip: !!showTooltip }}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
{required && <span style={{ color: '#f93920', paddingLeft: '2px' }}>*</span>}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
marginBottom: 6,
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'var(--semi-color-text-0)',
|
||||||
|
width: labelWidth || 118,
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
columnGap: 4,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TypeTag className="form-item-type-tag" type={type} />
|
||||||
|
{description ? <Tooltip content={description}>{renderTitle()}</Tooltip> : renderTitle(true)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { Field } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { TypeTag } from '../type-tag';
|
||||||
|
import { JsonSchema } from '../../typings';
|
||||||
|
import { FormOutputsContainer } from './styles';
|
||||||
|
|
||||||
|
export function FormOutputs() {
|
||||||
|
return (
|
||||||
|
<Field<JsonSchema> name={'outputs'}>
|
||||||
|
{({ field }) => {
|
||||||
|
const properties = field.value?.properties;
|
||||||
|
if (properties) {
|
||||||
|
const content = Object.keys(properties).map(key => {
|
||||||
|
const property = properties[key];
|
||||||
|
return <TypeTag key={key} name={key} type={property.type as string} />;
|
||||||
|
});
|
||||||
|
return <FormOutputsContainer>{content}</FormOutputsContainer>;
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
}}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const FormOutputsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border-top: 1px solid var(--semi-color-border);
|
||||||
|
padding: 8px 0 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:global(.semi-tag .semi-tag-content) {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { type SVGProps } from 'react';
|
||||||
|
|
||||||
|
import { Input, Button } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { FlowValueSchema, FlowRefValueSchema } from '../../typings';
|
||||||
|
import { VariableSelector } from '../../plugins/variable-plugin/variable-selector';
|
||||||
|
|
||||||
|
export function FxIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.581 4.49A2.75 2.75 0 0 1 8.319 2h.931a.75.75 0 0 1 0 1.5h-.931a1.25 1.25 0 0 0-1.245 1.131l-.083.869H9.25a.75.75 0 0 1 0 1.5H6.849l-.43 4.51A2.75 2.75 0 0 1 3.681 14H2.75a.75.75 0 0 1 0-1.5h.931a1.25 1.25 0 0 0 1.245-1.132L5.342 7H3.75a.75.75 0 0 1 0-1.5h1.735zM9.22 9.22a.75.75 0 0 1 1.06 0l1.22 1.22l1.22-1.22a.75.75 0 1 1 1.06 1.06l-1.22 1.22l1.22 1.22a.75.75 0 1 1-1.06 1.06l-1.22-1.22l-1.22 1.22a.75.75 0 1 1-1.06-1.06l1.22-1.22l-1.22-1.22a.75.75 0 0 1 0-1.06"
|
||||||
|
clipRule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FxExpressionProps {
|
||||||
|
value?: FlowValueSchema;
|
||||||
|
onChange: (value: FlowValueSchema) => void;
|
||||||
|
literal?: boolean;
|
||||||
|
hasError?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FxExpression(props: FxExpressionProps) {
|
||||||
|
const { value, onChange, disabled, literal } = props;
|
||||||
|
if (literal) return <Input value={value as string} onChange={onChange} disabled={disabled} />;
|
||||||
|
const isExpression = typeof value === 'object' && value.type === 'expression';
|
||||||
|
const toggleExpression = () => {
|
||||||
|
if (isExpression) {
|
||||||
|
onChange((value as FlowRefValueSchema).content as string);
|
||||||
|
} else {
|
||||||
|
onChange({ content: value as string, type: 'expression' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
{isExpression ? (
|
||||||
|
<VariableSelector
|
||||||
|
value={value.content}
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
|
onChange={v => onChange({ type: 'expression', content: v })}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={value as string}
|
||||||
|
onChange={onChange}
|
||||||
|
validateStatus={props.hasError ? 'error' : undefined}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flexGrow: 1, outline: props.hasError ? '1px solid red' : undefined }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!disabled && <Button theme="borderless" icon={<FxIcon />} onClick={toggleExpression} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
apps/demo-fixed-layout/src/form-components/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export * from './feedback';
|
||||||
|
export * from './form-content';
|
||||||
|
export * from './form-outputs';
|
||||||
|
export * from './form-inputs';
|
||||||
|
export * from './form-header';
|
||||||
|
export * from './form-item';
|
||||||
|
export * from './type-tag';
|
||||||
|
export * from './properties-edit';
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconPlus } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { JsonSchema } from '../../typings';
|
||||||
|
import { NodeRenderContext } from '../../context';
|
||||||
|
import { PropertyEdit } from './property-edit';
|
||||||
|
|
||||||
|
export interface PropertiesEditProps {
|
||||||
|
value?: Record<string, JsonSchema>;
|
||||||
|
onChange: (value: Record<string, JsonSchema>) => void;
|
||||||
|
useFx?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PropertiesEdit: React.FC<PropertiesEditProps> = props => {
|
||||||
|
const value = (props.value || {}) as Record<string, JsonSchema>;
|
||||||
|
const { readonly } = useContext(NodeRenderContext);
|
||||||
|
const [newProperty, updateNewPropertyFromCache] = useState<{ key: string; value: JsonSchema }>({
|
||||||
|
key: '',
|
||||||
|
value: { type: 'string' },
|
||||||
|
});
|
||||||
|
const [newPropertyVisible, setNewPropertyVisible] = useState<boolean>();
|
||||||
|
const clearCache = () => {
|
||||||
|
updateNewPropertyFromCache({ key: '', value: { type: 'string' } });
|
||||||
|
setNewPropertyVisible(false);
|
||||||
|
};
|
||||||
|
const updateProperty = (
|
||||||
|
propertyValue: JsonSchema,
|
||||||
|
propertyKey: string,
|
||||||
|
newPropertyKey?: string,
|
||||||
|
) => {
|
||||||
|
const newValue = { ...value };
|
||||||
|
if (newPropertyKey) {
|
||||||
|
delete newValue[propertyKey];
|
||||||
|
newValue[newPropertyKey] = propertyValue;
|
||||||
|
} else {
|
||||||
|
newValue[propertyKey] = propertyValue;
|
||||||
|
}
|
||||||
|
props.onChange(newValue);
|
||||||
|
};
|
||||||
|
const updateNewProperty = (
|
||||||
|
propertyValue: JsonSchema,
|
||||||
|
propertyKey: string,
|
||||||
|
newPropertyKey?: string,
|
||||||
|
) => {
|
||||||
|
// const newValue = { ...value }
|
||||||
|
if (newPropertyKey) {
|
||||||
|
if (!(newPropertyKey in value)) {
|
||||||
|
updateProperty(propertyValue, propertyKey, newPropertyKey);
|
||||||
|
}
|
||||||
|
clearCache();
|
||||||
|
} else {
|
||||||
|
updateNewPropertyFromCache({
|
||||||
|
key: newPropertyKey || propertyKey,
|
||||||
|
value: propertyValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.keys(props.value || {}).map(key => {
|
||||||
|
const property = (value[key] || {}) as JsonSchema;
|
||||||
|
return (
|
||||||
|
<PropertyEdit
|
||||||
|
key={key}
|
||||||
|
propertyKey={key}
|
||||||
|
useFx={props.useFx}
|
||||||
|
value={property}
|
||||||
|
disabled={readonly}
|
||||||
|
onChange={updateProperty}
|
||||||
|
onDelete={() => {
|
||||||
|
const newValue = { ...value };
|
||||||
|
delete newValue[key];
|
||||||
|
props.onChange(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{newPropertyVisible && (
|
||||||
|
<PropertyEdit
|
||||||
|
propertyKey={newProperty.key}
|
||||||
|
value={newProperty.value}
|
||||||
|
useFx={props.useFx}
|
||||||
|
onChange={updateNewProperty}
|
||||||
|
onDelete={() => {
|
||||||
|
const key = newProperty.key;
|
||||||
|
// after onblur
|
||||||
|
setTimeout(() => {
|
||||||
|
const newValue = { ...value };
|
||||||
|
delete newValue[key];
|
||||||
|
props.onChange(newValue);
|
||||||
|
clearCache();
|
||||||
|
}, 10);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!readonly && (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
theme="borderless"
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => setNewPropertyVisible(true)}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import React, { useState, useLayoutEffect } from 'react';
|
||||||
|
|
||||||
|
import { Input, Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconCrossCircleStroked } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { TypeSelector } from '../type-selector';
|
||||||
|
import { JsonSchema } from '../../typings';
|
||||||
|
import { VariableSelector } from '../../plugins/variable-plugin/variable-selector';
|
||||||
|
import { LeftColumn, Row } from './styles';
|
||||||
|
|
||||||
|
export interface PropertyEditProps {
|
||||||
|
propertyKey: string;
|
||||||
|
value: JsonSchema;
|
||||||
|
useFx?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (value: JsonSchema, propertyKey: string, newPropertyKey?: string) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PropertyEdit: React.FC<PropertyEditProps> = props => {
|
||||||
|
const { value, disabled } = props;
|
||||||
|
const [inputKey, updateKey] = useState(props.propertyKey);
|
||||||
|
const updateProperty = (key: keyof JsonSchema, val: any) => {
|
||||||
|
value[key] = val;
|
||||||
|
props.onChange(value, props.propertyKey);
|
||||||
|
};
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
updateKey(props.propertyKey);
|
||||||
|
}, [props.propertyKey]);
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<LeftColumn>
|
||||||
|
<TypeSelector
|
||||||
|
value={value.type}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ position: 'absolute', top: 6, left: 4, zIndex: 1 }}
|
||||||
|
onChange={val => updateProperty('type', val)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={inputKey}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={v => updateKey(v.trim())}
|
||||||
|
onBlur={() => {
|
||||||
|
if (inputKey !== '') {
|
||||||
|
props.onChange(value, props.propertyKey, inputKey);
|
||||||
|
} else {
|
||||||
|
updateKey(props.propertyKey);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ paddingLeft: 26 }}
|
||||||
|
/>
|
||||||
|
</LeftColumn>
|
||||||
|
{props.useFx ? (
|
||||||
|
<VariableSelector
|
||||||
|
value={value.default}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={val => updateProperty('default', val)}
|
||||||
|
style={{ flexGrow: 1, height: 32 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
disabled={disabled}
|
||||||
|
value={value.default}
|
||||||
|
onChange={val => updateProperty('default', val)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{props.onDelete && !disabled && (
|
||||||
|
<Button theme="borderless" icon={<IconCrossCircleStroked />} onClick={props.onDelete} />
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Row = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LeftColumn = styled.div`
|
||||||
|
width: 300px;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
51
apps/demo-fixed-layout/src/form-components/type-selector.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Tag, Dropdown } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { VariableTypeIcons } from '../plugins/variable-plugin/icons';
|
||||||
|
|
||||||
|
export interface TypeSelectorProps {
|
||||||
|
value?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (value?: string) => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
const dropdownMenus = ['object', 'boolean', 'array', 'string', 'integer', 'number'];
|
||||||
|
|
||||||
|
export const TypeSelector: React.FC<TypeSelectorProps> = props => {
|
||||||
|
const { value, disabled } = props;
|
||||||
|
const icon = VariableTypeIcons[value as any];
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
trigger="hover"
|
||||||
|
position="bottomRight"
|
||||||
|
disabled={disabled}
|
||||||
|
render={
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{dropdownMenus.map(key => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={key}
|
||||||
|
onClick={() => {
|
||||||
|
props.onChange?.(key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{VariableTypeIcons[key]}
|
||||||
|
<span style={{ paddingLeft: '4px' }}>{key}</span>
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
color="white"
|
||||||
|
style={props.style}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Tag>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
49
apps/demo-fixed-layout/src/form-components/type-tag.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { Tag, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { VariableTypeIcons, ArrayIcons } from '../plugins/variable-plugin/icons';
|
||||||
|
|
||||||
|
interface PropsType {
|
||||||
|
name?: string | JSX.Element;
|
||||||
|
type: string;
|
||||||
|
className?: string;
|
||||||
|
isArray?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TooltipContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function TypeTag({ name, type, isArray, className }: PropsType) {
|
||||||
|
const icon = isArray ? ArrayIcons[type] : VariableTypeIcons[type];
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<TooltipContainer>
|
||||||
|
{icon} {type}
|
||||||
|
</TooltipContainer>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tag color="white" className={className} style={{ padding: 4, maxWidth: 450 }}>
|
||||||
|
{icon}
|
||||||
|
{name && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
marginLeft: 4,
|
||||||
|
marginTop: -1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
apps/demo-fixed-layout/src/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { useEditorProps } from './use-editor-props';
|
||||||
241
apps/demo-fixed-layout/src/hooks/use-editor-props.ts
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
|
||||||
|
import { createGroupPlugin } from '@flowgram.ai/group-plugin';
|
||||||
|
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
|
||||||
|
import {
|
||||||
|
FixedLayoutProps,
|
||||||
|
FlowDocumentJSON,
|
||||||
|
FlowLayoutDefault,
|
||||||
|
FlowRendererKey,
|
||||||
|
ShortcutsRegistry,
|
||||||
|
} from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { type FlowNodeRegistry } from '../typings';
|
||||||
|
import { shortcutGetter } from '../shortcuts';
|
||||||
|
import { GroupBoxHeader, GroupNode } from '../plugins/group-plugin';
|
||||||
|
import { createVariablePlugin, createClipboardPlugin } from '../plugins';
|
||||||
|
import { defaultFormMeta } from '../nodes';
|
||||||
|
import { SelectorBoxPopover } from '../components/selector-box-popover';
|
||||||
|
import NodeAdder from '../components/node-adder';
|
||||||
|
import BranchAdder from '../components/branch-adder';
|
||||||
|
import { BaseNode } from '../components/base-node';
|
||||||
|
import { DragNode } from '../components';
|
||||||
|
|
||||||
|
export function useEditorProps(
|
||||||
|
initialData: FlowDocumentJSON,
|
||||||
|
nodeRegistries: FlowNodeRegistry[],
|
||||||
|
): FixedLayoutProps {
|
||||||
|
return useMemo<FixedLayoutProps>(
|
||||||
|
() => ({
|
||||||
|
/**
|
||||||
|
* Whether to enable the background
|
||||||
|
*/
|
||||||
|
background: true,
|
||||||
|
/**
|
||||||
|
* Whether it is read-only or not, the node cannot be dragged in read-only mode
|
||||||
|
*/
|
||||||
|
readonly: false,
|
||||||
|
/**
|
||||||
|
* Initial data
|
||||||
|
* 初始化数据
|
||||||
|
*/
|
||||||
|
initialData,
|
||||||
|
/**
|
||||||
|
* Node registries
|
||||||
|
* 节点注册
|
||||||
|
*/
|
||||||
|
nodeRegistries,
|
||||||
|
/**
|
||||||
|
* Get the default node registry, which will be merged with the 'nodeRegistries'
|
||||||
|
* 提供默认的节点注册,这个会和 nodeRegistries 做合并
|
||||||
|
*/
|
||||||
|
getNodeDefaultRegistry(type) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
meta: {
|
||||||
|
/**
|
||||||
|
* Default expanded
|
||||||
|
* 默认展开所有节点
|
||||||
|
*/
|
||||||
|
defaultExpanded: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Default FormMeta
|
||||||
|
* 默认的表单配置
|
||||||
|
*/
|
||||||
|
formMeta: defaultFormMeta,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Set default layout
|
||||||
|
*/
|
||||||
|
defaultLayout: FlowLayoutDefault.VERTICAL_FIXED_LAYOUT, // or FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT
|
||||||
|
/**
|
||||||
|
* Style config
|
||||||
|
*/
|
||||||
|
// constants: {
|
||||||
|
// [ConstantKeys.NODE_SPACING]: 24,
|
||||||
|
// [ConstantKeys.INLINE_SPACING_BOTTOM]: 24,
|
||||||
|
// [ConstantKeys.INLINE_BLOCKS_INLINE_SPACING_BOTTOM]: 13,
|
||||||
|
// [ConstantKeys.ROUNDED_LINE_X_RADIUS]: 8,
|
||||||
|
// [ConstantKeys.ROUNDED_LINE_Y_RADIUS]: 10,
|
||||||
|
// [ConstantKeys.INLINE_BLOCKS_INLINE_SPACING_TOP]: 23,
|
||||||
|
// [ConstantKeys.INLINE_BLOCKS_PADDING_BOTTOM]: 30,
|
||||||
|
// [ConstantKeys.COLLAPSED_SPACING]: 10,
|
||||||
|
// [ConstantKeys.BASE_COLOR]: '#B8BCC1',
|
||||||
|
// [ConstantKeys.BASE_ACTIVATED_COLOR]: '#82A7FC',
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* SelectBox config
|
||||||
|
*/
|
||||||
|
selectBox: {
|
||||||
|
SelectorBoxPopover,
|
||||||
|
},
|
||||||
|
// Config shortcuts
|
||||||
|
shortcuts: (registry: ShortcutsRegistry, ctx) => {
|
||||||
|
registry.addHandlers(...shortcutGetter.map(getter => getter(ctx)));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Drag/Drop config
|
||||||
|
*/
|
||||||
|
dragdrop: {
|
||||||
|
/**
|
||||||
|
* Callback when drag drop
|
||||||
|
*/
|
||||||
|
onDrop: (ctx, dropData) => {
|
||||||
|
// console.log(
|
||||||
|
// '>>> onDrop: ',
|
||||||
|
// dropData.dropNode.id,
|
||||||
|
// dropData.dragNodes.map(n => n.id),
|
||||||
|
// );
|
||||||
|
},
|
||||||
|
canDrop: (ctx, dropData) =>
|
||||||
|
// console.log(
|
||||||
|
// '>>> canDrop: ',
|
||||||
|
// dropData.isBranch,
|
||||||
|
// dropData.dropNode.id,
|
||||||
|
// dropData.dragNodes.map(n => n.id),
|
||||||
|
// );
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Redo/Undo enable
|
||||||
|
*/
|
||||||
|
history: {
|
||||||
|
enable: true,
|
||||||
|
enableChangeNode: true, // Listen Node engine data change
|
||||||
|
onApply(ctx, opt) {
|
||||||
|
// Listen change to trigger auto save
|
||||||
|
// console.log('auto save: ', ctx.document.toJSON(), opt);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Node engine enable, you can configure formMeta in the FlowNodeRegistry
|
||||||
|
*/
|
||||||
|
nodeEngine: {
|
||||||
|
enable: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Variable engine enable
|
||||||
|
*/
|
||||||
|
variableEngine: {
|
||||||
|
enable: true,
|
||||||
|
layout: 'fixed',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Materials, components can be customized based on the key
|
||||||
|
* @see @see https://github.com/coze-dev/flowgram.ai/blob/main/packages/materials/fixed-semi-materials/src/components/index.tsx
|
||||||
|
* 可以通过 key 自定义 UI 组件
|
||||||
|
*/
|
||||||
|
materials: {
|
||||||
|
renderNodes: {
|
||||||
|
...defaultFixedSemiMaterials,
|
||||||
|
[FlowRendererKey.ADDER]: NodeAdder,
|
||||||
|
[FlowRendererKey.BRANCH_ADDER]: BranchAdder,
|
||||||
|
[FlowRendererKey.DRAG_NODE]: DragNode,
|
||||||
|
},
|
||||||
|
renderDefaultNode: BaseNode, // node render
|
||||||
|
renderTexts: {
|
||||||
|
'loop-end-text': 'Loop End',
|
||||||
|
'loop-while-text': 'Condition Satisfied',
|
||||||
|
'loop-traverse-text': 'Loop',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Playground init
|
||||||
|
*/
|
||||||
|
onInit: ctx => {
|
||||||
|
/**
|
||||||
|
* Data can also be dynamically loaded via fromJSON
|
||||||
|
* 也可以通过 fromJSON 动态加载数据
|
||||||
|
*/
|
||||||
|
// ctx.document.fromJSON(initialData)
|
||||||
|
console.log('---- Playground Init ----');
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Playground render
|
||||||
|
*/
|
||||||
|
onAllLayersRendered: ctx => {
|
||||||
|
setTimeout(() => {
|
||||||
|
ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));
|
||||||
|
}, 10);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Playground dispose
|
||||||
|
*/
|
||||||
|
onDispose: () => {
|
||||||
|
console.log('---- Playground Dispose ----');
|
||||||
|
},
|
||||||
|
plugins: () => [
|
||||||
|
/**
|
||||||
|
* Minimap plugin
|
||||||
|
* 缩略图插件
|
||||||
|
*/
|
||||||
|
createMinimapPlugin({
|
||||||
|
disableLayer: true,
|
||||||
|
enableDisplayAllNodes: true,
|
||||||
|
canvasStyle: {
|
||||||
|
canvasWidth: 182,
|
||||||
|
canvasHeight: 102,
|
||||||
|
canvasPadding: 50,
|
||||||
|
canvasBackground: 'rgba(245, 245, 245, 1)',
|
||||||
|
canvasBorderRadius: 10,
|
||||||
|
viewportBackground: 'rgba(235, 235, 235, 1)',
|
||||||
|
viewportBorderRadius: 4,
|
||||||
|
viewportBorderColor: 'rgba(201, 201, 201, 1)',
|
||||||
|
viewportBorderWidth: 1,
|
||||||
|
viewportBorderDashLength: 2,
|
||||||
|
nodeColor: 'rgba(255, 255, 255, 1)',
|
||||||
|
nodeBorderRadius: 2,
|
||||||
|
nodeBorderWidth: 0.145,
|
||||||
|
nodeBorderColor: 'rgba(6, 7, 9, 0.10)',
|
||||||
|
overlayColor: 'rgba(255, 255, 255, 0)',
|
||||||
|
},
|
||||||
|
inactiveDebounceTime: 1,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* Group plugin
|
||||||
|
* 分组插件
|
||||||
|
*/
|
||||||
|
createGroupPlugin({
|
||||||
|
components: {
|
||||||
|
GroupBoxHeader,
|
||||||
|
GroupNode,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* Variable plugin
|
||||||
|
* 变量插件
|
||||||
|
*/
|
||||||
|
createVariablePlugin({}),
|
||||||
|
/**
|
||||||
|
* Clipboard plugin
|
||||||
|
* 剪切板插件
|
||||||
|
*/
|
||||||
|
createClipboardPlugin(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
1
apps/demo-fixed-layout/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Editor as DemoFixedLayout } from './editor';
|
||||||
130
apps/demo-fixed-layout/src/initial-data.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { FlowDocumentJSON } from './typings';
|
||||||
|
|
||||||
|
export const initialData: FlowDocumentJSON = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'start_0',
|
||||||
|
type: 'start',
|
||||||
|
blocks: [],
|
||||||
|
data: {
|
||||||
|
title: 'Start',
|
||||||
|
outputs: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'Hello Flow.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'llm_0',
|
||||||
|
type: 'llm',
|
||||||
|
blocks: [],
|
||||||
|
data: {
|
||||||
|
title: 'LLM',
|
||||||
|
inputsValues: {
|
||||||
|
modelType: 'gpt-3.5-turbo',
|
||||||
|
temperature: 0.5,
|
||||||
|
systemPrompt: 'You are an AI assistant.',
|
||||||
|
prompt: '',
|
||||||
|
},
|
||||||
|
inputs: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['modelType', 'temperature', 'prompt'],
|
||||||
|
properties: {
|
||||||
|
modelType: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
temperature: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
systemPrompt: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
result: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'loop_0',
|
||||||
|
type: 'loop',
|
||||||
|
data: {
|
||||||
|
title: 'Loop',
|
||||||
|
},
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: 'condition_0',
|
||||||
|
type: 'condition',
|
||||||
|
data: {
|
||||||
|
title: 'Condition',
|
||||||
|
},
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: 'branch_0',
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'If_0',
|
||||||
|
inputs: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['condition'],
|
||||||
|
properties: {
|
||||||
|
condition: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
blocks: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'branch_1',
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'If_1',
|
||||||
|
inputs: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['condition'],
|
||||||
|
properties: {
|
||||||
|
condition: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {},
|
||||||
|
blocks: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'end_0',
|
||||||
|
type: 'end',
|
||||||
|
blocks: [],
|
||||||
|
data: {
|
||||||
|
title: 'End',
|
||||||
|
outputs: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
result: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/demo-fixed-layout/src/nodes/block/index.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
|
import iconIf from '../../assets/icon-if.png';
|
||||||
|
|
||||||
|
let id = 2;
|
||||||
|
export const BlockNodeRegistry: FlowNodeRegistry = {
|
||||||
|
type: 'block',
|
||||||
|
meta: {
|
||||||
|
copyDisable: true,
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
icon: iconIf,
|
||||||
|
description: 'Execute the branch when the condition is met.',
|
||||||
|
},
|
||||||
|
canAdd: () => false,
|
||||||
|
onAdd(ctx, from) {
|
||||||
|
return {
|
||||||
|
id: `if_${nanoid(5)}`,
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: `If_${id++}`,
|
||||||
|
inputs: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['condition'],
|
||||||
|
properties: {
|
||||||
|
condition: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
58
apps/demo-fixed-layout/src/nodes/condition/index.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { FlowNodeSplitType } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
|
import iconCondition from '../../assets/icon-condition.svg';
|
||||||
|
|
||||||
|
export const ConditionNodeRegistry: FlowNodeRegistry = {
|
||||||
|
extend: FlowNodeSplitType.DYNAMIC_SPLIT,
|
||||||
|
type: 'condition',
|
||||||
|
info: {
|
||||||
|
icon: iconCondition,
|
||||||
|
description:
|
||||||
|
'Connect multiple downstream branches. Only the corresponding branch will be executed if the set conditions are met.',
|
||||||
|
},
|
||||||
|
onAdd() {
|
||||||
|
return {
|
||||||
|
id: `condition_${nanoid(5)}`,
|
||||||
|
type: 'condition',
|
||||||
|
data: {
|
||||||
|
title: 'Condition',
|
||||||
|
},
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: nanoid(5),
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'If_0',
|
||||||
|
inputs: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['condition'],
|
||||||
|
properties: {
|
||||||
|
condition: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(5),
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'If_1',
|
||||||
|
inputs: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['condition'],
|
||||||
|
properties: {
|
||||||
|
condition: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
30
apps/demo-fixed-layout/src/nodes/default-form-meta.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { FlowNodeJSON } from '../typings';
|
||||||
|
import { FormHeader, FormContent, FormInputs, FormOutputs } from '../form-components';
|
||||||
|
|
||||||
|
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||||
|
<>
|
||||||
|
<FormHeader />
|
||||||
|
<FormContent>
|
||||||
|
<FormInputs />
|
||||||
|
<FormOutputs />
|
||||||
|
</FormContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const defaultFormMeta: FormMeta<FlowNodeJSON> = {
|
||||||
|
render: renderForm,
|
||||||
|
validateTrigger: ValidateTrigger.onChange,
|
||||||
|
validate: {
|
||||||
|
title: ({ value }) => (value ? undefined : 'Title is required'),
|
||||||
|
'inputsValues.*': ({ value, context, formValues, name }) => {
|
||||||
|
const valuePropetyKey = name.replace(/^inputsValues\./, '');
|
||||||
|
const required = formValues.inputs?.required || [];
|
||||||
|
if (required.includes(valuePropetyKey) && (value === '' || value === undefined)) {
|
||||||
|
return `${valuePropetyKey} is required`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
38
apps/demo-fixed-layout/src/nodes/end/form-meta.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldRenderProps,
|
||||||
|
FormRenderProps,
|
||||||
|
FormMeta,
|
||||||
|
ValidateTrigger,
|
||||||
|
} from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { FlowNodeJSON, JsonSchema } from '../../typings';
|
||||||
|
import { FormHeader, FormContent, FormOutputs, PropertiesEdit } from '../../form-components';
|
||||||
|
|
||||||
|
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||||
|
<>
|
||||||
|
<FormHeader />
|
||||||
|
<FormContent>
|
||||||
|
<Field
|
||||||
|
name="outputs.properties"
|
||||||
|
render={({
|
||||||
|
field: { value, onChange },
|
||||||
|
fieldState,
|
||||||
|
}: FieldRenderProps<Record<string, JsonSchema>>) => (
|
||||||
|
<>
|
||||||
|
<PropertiesEdit value={value} onChange={onChange} useFx={true} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormOutputs />
|
||||||
|
</FormContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||||
|
render: renderForm,
|
||||||
|
validateTrigger: ValidateTrigger.onChange,
|
||||||
|
validate: {
|
||||||
|
title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
|
||||||
|
},
|
||||||
|
};
|
||||||
47
apps/demo-fixed-layout/src/nodes/end/index.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
|
import iconEnd from '../../assets/icon-end.jpg';
|
||||||
|
import { formMeta } from './form-meta';
|
||||||
|
|
||||||
|
export const EndNodeRegistry: FlowNodeRegistry = {
|
||||||
|
type: 'end',
|
||||||
|
meta: {
|
||||||
|
isNodeEnd: true, // Mark as end
|
||||||
|
selectable: false, // End node cannot select
|
||||||
|
copyDisable: true, // End node canot copy
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
icon: iconEnd,
|
||||||
|
description:
|
||||||
|
'The final node of the workflow, used to return the result information after the workflow is run.',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Render node via formMeta
|
||||||
|
*/
|
||||||
|
formMeta,
|
||||||
|
canAdd(ctx, from) {
|
||||||
|
// You can only add to the last node of the branch
|
||||||
|
return from.isLast;
|
||||||
|
},
|
||||||
|
canDelete(ctx, node) {
|
||||||
|
return node.parent === ctx.document.root;
|
||||||
|
},
|
||||||
|
onAdd(ctx, from) {
|
||||||
|
return {
|
||||||
|
id: `end_${nanoid()}`,
|
||||||
|
type: 'end',
|
||||||
|
data: {
|
||||||
|
title: 'End',
|
||||||
|
outputs: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
result: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
18
apps/demo-fixed-layout/src/nodes/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { type FlowNodeRegistry } from '../typings';
|
||||||
|
import { StartNodeRegistry } from './start';
|
||||||
|
import { LoopNodeRegistry } from './loop';
|
||||||
|
import { LLMNodeRegistry } from './llm';
|
||||||
|
import { EndNodeRegistry } from './end';
|
||||||
|
import { ConditionNodeRegistry } from './condition';
|
||||||
|
import { BlockNodeRegistry } from './block';
|
||||||
|
|
||||||
|
export const FlowNodeRegistries: FlowNodeRegistry[] = [
|
||||||
|
StartNodeRegistry,
|
||||||
|
EndNodeRegistry,
|
||||||
|
ConditionNodeRegistry,
|
||||||
|
LLMNodeRegistry,
|
||||||
|
LoopNodeRegistry,
|
||||||
|
BlockNodeRegistry,
|
||||||
|
];
|
||||||
|
|
||||||
|
export { defaultFormMeta } from './default-form-meta';
|
||||||
48
apps/demo-fixed-layout/src/nodes/llm/index.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
|
import iconLLM from '../../assets/icon-llm.jpg';
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
export const LLMNodeRegistry: FlowNodeRegistry = {
|
||||||
|
type: 'llm',
|
||||||
|
info: {
|
||||||
|
icon: iconLLM,
|
||||||
|
description:
|
||||||
|
'Call the large language model and use variables and prompt words to generate responses.',
|
||||||
|
},
|
||||||
|
onAdd() {
|
||||||
|
return {
|
||||||
|
id: `llm_${nanoid(5)}`,
|
||||||
|
type: 'llm',
|
||||||
|
data: {
|
||||||
|
title: `LLM_${++index}`,
|
||||||
|
inputsValues: {},
|
||||||
|
inputs: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['modelType', 'temperature', 'prompt'],
|
||||||
|
properties: {
|
||||||
|
modelType: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
temperature: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
systemPrompt: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
result: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
22
apps/demo-fixed-layout/src/nodes/loop/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
|
import iconLoop from '../../assets/icon-loop.svg';
|
||||||
|
|
||||||
|
export const LoopNodeRegistry: FlowNodeRegistry = {
|
||||||
|
type: 'loop',
|
||||||
|
info: {
|
||||||
|
icon: iconLoop,
|
||||||
|
description:
|
||||||
|
'Used to repeatedly execute a series of tasks by setting the number of iterations and logic',
|
||||||
|
},
|
||||||
|
onAdd() {
|
||||||
|
return {
|
||||||
|
id: `loop_${nanoid(5)}`,
|
||||||
|
type: 'loop',
|
||||||
|
data: {
|
||||||
|
title: 'Loop',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
38
apps/demo-fixed-layout/src/nodes/start/form-meta.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldRenderProps,
|
||||||
|
FormRenderProps,
|
||||||
|
FormMeta,
|
||||||
|
ValidateTrigger,
|
||||||
|
} from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { FlowNodeJSON, JsonSchema } from '../../typings';
|
||||||
|
import { FormHeader, FormContent, FormOutputs, PropertiesEdit } from '../../form-components';
|
||||||
|
|
||||||
|
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||||
|
<>
|
||||||
|
<FormHeader />
|
||||||
|
<FormContent>
|
||||||
|
<Field
|
||||||
|
name="outputs.properties"
|
||||||
|
render={({
|
||||||
|
field: { value, onChange },
|
||||||
|
fieldState,
|
||||||
|
}: FieldRenderProps<Record<string, JsonSchema>>) => (
|
||||||
|
<>
|
||||||
|
<PropertiesEdit value={value} onChange={onChange} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormOutputs />
|
||||||
|
</FormContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||||
|
render: renderForm,
|
||||||
|
validateTrigger: ValidateTrigger.onChange,
|
||||||
|
validate: {
|
||||||
|
title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
|
||||||
|
},
|
||||||
|
};
|
||||||