├── backend
├── apps
│ ├── __init__.py
│ ├── manage
│ │ ├── __init__.py
│ │ ├── migrations
│ │ │ ├── __init__.py
│ │ │ └── 0001_initial.py
│ │ ├── templatetags
│ │ │ ├── __init__.py
│ │ │ └── loonflow_filter.py
│ │ ├── urls.py
│ │ └── models.py
│ ├── util
│ │ ├── __init__.py
│ │ ├── migrations
│ │ │ ├── __init__.py
│ │ │ └── 0001_initial.py
│ │ ├── tests.py
│ │ ├── admin.py
│ │ ├── views.py
│ │ ├── apps.py
│ │ └── models.py
│ ├── ticket
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── __init__.py
│ │ ├── tests.py
│ │ ├── apps.py
│ │ └── urls.py
│ ├── workflow
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── __init__.py
│ │ ├── tests.py
│ │ ├── apps.py
│ │ └── urls.py
│ ├── account
│ │ ├── __init__.py
│ │ ├── tests.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── apps.py
│ │ └── urls.py
│ ├── loon_model_base_admin.py
│ ├── archived_model.py
│ ├── homepage_view.py
│ └── loon_base_view.py
├── service
│ ├── __init__.py
│ ├── hook
│ │ ├── __init__.py
│ │ └── hook_base_service.py
│ ├── util
│ │ ├── __init__.py
│ │ ├── link_service.py
│ │ ├── archive_service.py
│ │ └── encrypt_service.py
│ ├── account
│ │ ├── __init__.py
│ │ ├── account_auth_service.py
│ │ └── account_tenant_service.py
│ ├── common
│ │ ├── __init__.py
│ │ ├── log_service.py
│ │ └── schema_valid_service.py
│ ├── exception
│ │ ├── __init__.py
│ │ └── custom_common_exception.py
│ ├── manage
│ │ ├── __init__.py
│ │ ├── overview_service.py
│ │ └── common_config_service.py
│ ├── permission
│ │ ├── __init__.py
│ │ └── user_permission.py
│ ├── ticket
│ │ └── __init__.py
│ ├── workflow
│ │ └── __init__.py
│ ├── base_service.py
│ ├── csrf_service.py
│ ├── format_response.py
│ └── redis_pool.py
├── settings
│ ├── __init__.py
│ ├── test.py
│ └── test.py.sample
├── tests
│ ├── __init__.py
│ ├── test_models
│ │ ├── __init__.py
│ │ └── test_account_model.py
│ ├── test_views
│ │ ├── __init__.py
│ │ ├── test_account_view.py
│ │ └── test_workflow_view.py
│ ├── test_services
│ │ └── __init__.py
│ ├── set_test_env.sh
│ ├── fixtures
│ │ └── workflows.json
│ └── base.py
├── requirements
│ ├── dev.txt
│ ├── pro.txt
│ ├── test.txt
│ └── common.txt
├── static
│ └── images
│ │ └── tenant0001.png
├── loonflow
│ ├── __init__.py
│ ├── contexts.py
│ ├── wsgi.py
│ └── urls.py
├── .coveragerc
├── manage_test.py
└── manage.py
├── docker_compose_deploy
├── __init__.py
├── .env
├── loonflow-ui
│ ├── Dockerfile
│ └── nginx.conf
└── loonflow-backend
│ └── Dockerfile
├── frontend
├── src
│ ├── types
│ │ ├── draft-js.d.ts
│ │ ├── readme.md
│ │ ├── common.ts
│ │ ├── notification.ts
│ │ ├── role.ts
│ │ ├── dept.ts
│ │ ├── tenant.ts
│ │ ├── user.ts
│ │ └── application.ts
│ ├── react-app-env.d.ts
│ ├── components
│ │ ├── Setting
│ │ │ ├── Application
│ │ │ │ └── index.tsx
│ │ │ └── Notification
│ │ │ │ └── index.tsx
│ │ ├── commonComponents
│ │ │ ├── Snackbar
│ │ │ │ ├── index.tsx
│ │ │ │ ├── GlobalSnackbar.tsx
│ │ │ │ └── SnackbarProvider.tsx
│ │ │ └── DynamicLocalizationProvider.tsx
│ │ ├── Ticket
│ │ │ ├── AllTicket
│ │ │ │ └── index.tsx
│ │ │ ├── OwnerTicket
│ │ │ │ └── index.tsx
│ │ │ ├── ViewTicket
│ │ │ │ └── index.tsx
│ │ │ ├── DutyTicket
│ │ │ │ └── index.tsx
│ │ │ ├── RelationTicket
│ │ │ │ └── index.tsx
│ │ │ ├── InterveneTicket
│ │ │ │ └── index.tsx
│ │ │ ├── TicketDetailPage
│ │ │ │ └── index.tsx
│ │ │ └── NewTicketPage
│ │ │ │ └── index.tsx
│ │ ├── Workflow
│ │ │ └── WorkflowDetail
│ │ │ │ ├── WorkflowValidation
│ │ │ │ ├── types.ts
│ │ │ │ ├── i18n.ts
│ │ │ │ ├── edgeConditionValidation.ts
│ │ │ │ ├── normalNodeValidation.ts
│ │ │ │ ├── formValidation.ts
│ │ │ │ ├── timerNodeValidation.ts
│ │ │ │ ├── connectivityValidation.ts
│ │ │ │ └── exclusiveNodeValidation.ts
│ │ │ │ └── checkWorkflowCompatibility.ts
│ │ ├── formFields
│ │ │ ├── WorkflowInfoField.tsx
│ │ │ ├── TicketNodesField.tsx
│ │ │ ├── TicketCurrentAssigneeInfosField.tsx
│ │ │ ├── TicketCreateAtField.tsx
│ │ │ ├── TicketActStateField.tsx
│ │ │ ├── TicketCreatorField.tsx
│ │ │ ├── types.ts
│ │ │ ├── TextField.tsx
│ │ │ └── TextAreaField.tsx
│ │ ├── home
│ │ │ └── HomePage.tsx
│ │ ├── home2
│ │ │ └── HomePage.tsx
│ │ └── Organization
│ │ │ └── UserDept
│ │ │ └── index.tsx
│ ├── loonflow.svg
│ ├── Loonflow_logo.png
│ ├── i18n
│ │ └── locales
│ │ │ ├── zh-CN
│ │ │ ├── department.json
│ │ │ ├── signIn.json
│ │ │ ├── menu.json
│ │ │ ├── layout.json
│ │ │ ├── role.json
│ │ │ ├── user.json
│ │ │ ├── setting.json
│ │ │ └── ticket.json
│ │ │ └── en-US
│ │ │ ├── department.json
│ │ │ ├── signIn.json
│ │ │ ├── menu.json
│ │ │ ├── layout.json
│ │ │ ├── role.json
│ │ │ ├── user.json
│ │ │ ├── setting.json
│ │ │ └── ticket.json
│ ├── setupTests.ts
│ ├── App.test.tsx
│ ├── services
│ │ ├── authService.ts
│ │ ├── tenant.ts
│ │ ├── API.d.ts
│ │ ├── api.ts
│ │ ├── notification.ts
│ │ └── role.ts
│ ├── index.css
│ ├── hooks
│ │ └── useSnackbar.ts
│ ├── reportWebVitals.ts
│ ├── utils
│ │ ├── PrivateRoute.tsx
│ │ ├── jwt.ts
│ │ └── cookie.ts
│ ├── App.css
│ ├── store
│ │ ├── tenantSlice.ts
│ │ ├── index.ts
│ │ └── authSlice.ts
│ └── index.tsx
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── loonflow_logo.png
│ ├── loonflow_logo1.png
│ ├── manifest.json
│ └── index.html
├── .yarn
│ └── install-state.gz
├── .env
├── .yarnrc.yml
├── craco.config.js
├── tsc-fast.js
└── tsconfig.json
├── static
└── images
│ └── donation_code.png
├── sphinx_docs
├── source
│ ├── images
│ │ ├── tenant_info.png
│ │ ├── allow_withdraw.png
│ │ ├── donation_code.png
│ │ ├── new_workflow.png
│ │ ├── title_property.png
│ │ ├── category_tickets.png
│ │ ├── fill_ticket_form.png
│ │ ├── release_workflow.png
│ │ ├── role_management.png
│ │ ├── select_workflow.png
│ │ ├── workflow_diagram.png
│ │ ├── ticket_detail_page.png
│ │ ├── voluntary_pick_up.png
│ │ ├── use_the_new_workflow.png
│ │ ├── user_dept_management.png
│ │ ├── application_management.png
│ │ ├── fill_workflow_basic_info.png
│ │ ├── notification_management.png
│ │ ├── workbench_duty_tickets.png
│ │ ├── fill_workflow_form_design.png
│ │ ├── workflow_version_management.png
│ │ ├── fill_workflow_process_design01.png
│ │ ├── fill_workflow_process_design02.png
│ │ ├── fill_workflow_process_design03.png
│ │ └── start_node_title_auto_generate.png
│ ├── api_reference
│ │ └── postman.rst
│ ├── others
│ │ └── release_notes
│ │ │ ├── r1.x
│ │ │ ├── index.rst
│ │ │ └── r1.0.0.rst
│ │ │ ├── r2.x
│ │ │ ├── index.rst
│ │ │ └── r2.0.0.rst
│ │ │ ├── r3.x
│ │ │ ├── index.rst
│ │ │ ├── r3.0.1+.rst
│ │ │ └── r3.0.0.rst
│ │ │ ├── r0.x
│ │ │ └── index.rst
│ │ │ └── index.rst
│ ├── user_guide
│ │ ├── organization
│ │ │ ├── index.rst
│ │ │ ├── role.rst
│ │ │ └── user_dept.rst
│ │ ├── setting
│ │ │ ├── index.rst
│ │ │ ├── application.rst
│ │ │ ├── tenant.rst
│ │ │ └── notification.rst
│ │ ├── workflow
│ │ │ ├── index.rst
│ │ │ └── workflow_version.rst
│ │ └── ticket
│ │ │ ├── index.rst
│ │ │ ├── submit_ticket.rst
│ │ │ ├── handle_ticket.rst
│ │ │ └── query_ticket.rst
│ ├── core_concepts
│ │ └── ticket_system_fundamentals
│ │ │ └── index.rst
│ ├── introduction
│ │ ├── welcome
│ │ │ ├── index.rst
│ │ │ ├── about_loonflow.rst
│ │ │ └── getting_help.rst
│ │ └── who_need_read.rst
│ ├── installtion_deployment
│ │ ├── installation_guide
│ │ │ └── index.rst
│ │ └── system_requirements
│ │ │ ├── index.rst
│ │ │ └── os_environment_requirements.rst
│ ├── locale
│ │ └── zh_CN
│ │ │ └── LC_MESSAGES
│ │ │ ├── user_guide
│ │ │ ├── setting
│ │ │ │ ├── index.po
│ │ │ │ ├── application.po
│ │ │ │ └── tenant.po
│ │ │ ├── ticket
│ │ │ │ └── index.po
│ │ │ ├── workflow
│ │ │ │ ├── index.po
│ │ │ │ └── workflow_version.po
│ │ │ └── organization
│ │ │ │ ├── index.po
│ │ │ │ └── role.po
│ │ │ ├── introduction
│ │ │ ├── welcome
│ │ │ │ ├── index.po
│ │ │ │ ├── about_loonflow.po
│ │ │ │ └── getting_help.po
│ │ │ └── who_need_read.po
│ │ │ ├── others
│ │ │ └── release_notes
│ │ │ │ ├── r1.x
│ │ │ │ └── index.po
│ │ │ │ ├── r2.x
│ │ │ │ ├── index.po
│ │ │ │ └── r2.0.0.po
│ │ │ │ ├── r3.x
│ │ │ │ └── index.po
│ │ │ │ ├── index.po
│ │ │ │ └── r0.x
│ │ │ │ └── index.po
│ │ │ ├── installtion_deployment
│ │ │ ├── installation_guide
│ │ │ │ └── index.po
│ │ │ └── system_requirements
│ │ │ │ └── index.po
│ │ │ ├── core_concepts
│ │ │ └── ticket_system_fundamentals
│ │ │ │ └── index.po
│ │ │ ├── api_reference.po
│ │ │ ├── api_reference
│ │ │ └── postman.po
│ │ │ └── index.po
│ └── index.rst
├── README.md
├── requirements.txt
├── make.bat
├── Makefile
└── .readthedocs.yaml
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── bug_report.yaml
│ └── feature_request.yaml
├── workflows
│ ├── unit_test.yml.disabled
│ └── image_build_backend.yml
└── ISSUE_TEMPLATE.md
├── Roadmap_zh.md
└── .gitignore
/backend/apps/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/settings/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/apps/manage/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/apps/util/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/hook/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/util/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docker_compose_deploy/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/account/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/common/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/exception/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/manage/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/permission/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/ticket/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/service/workflow/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/tests/test_models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/tests/test_views/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/apps/manage/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/apps/ticket/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/apps/util/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/tests/test_services/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/apps/manage/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/apps/workflow/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/types/draft-js.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'draft-js';
--------------------------------------------------------------------------------
/backend/requirements/dev.txt:
--------------------------------------------------------------------------------
1 | #requrements/dev.txt
2 | -r common.txt
3 |
--------------------------------------------------------------------------------
/backend/service/base_service.py:
--------------------------------------------------------------------------------
1 | class BaseService(object):
2 | pass
3 |
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/backend/apps/ticket/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'apps.ticket.apps.TicketConfig'
2 |
--------------------------------------------------------------------------------
/backend/requirements/pro.txt:
--------------------------------------------------------------------------------
1 | #requrements/pro.txt
2 | -r common.txt
3 | gunicorn==22.0.0
--------------------------------------------------------------------------------
/backend/apps/account/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'apps.account.apps.AccountConfig'
2 |
--------------------------------------------------------------------------------
/backend/apps/util/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/apps/account/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/apps/ticket/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/apps/util/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/backend/apps/util/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 | # Create your views here.
4 |
--------------------------------------------------------------------------------
/backend/apps/workflow/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'apps.workflow.apps.WorkflowConfig'
2 |
3 |
--------------------------------------------------------------------------------
/backend/apps/workflow/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/apps/account/migrations/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'apps.workflow.apps.WorkflowConfig'
2 |
--------------------------------------------------------------------------------
/backend/requirements/test.txt:
--------------------------------------------------------------------------------
1 | #requrements/test.txt
2 | -r common.txt
3 | mock==4.0.2
4 | coverage==5.2.1
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/components/Setting/Application/index.tsx:
--------------------------------------------------------------------------------
1 | export { ApplicationList } from './ApplicationList';
--------------------------------------------------------------------------------
/frontend/src/loonflow.svg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/src/loonflow.svg
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/.yarn/install-state.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/.yarn/install-state.gz
--------------------------------------------------------------------------------
/frontend/src/Loonflow_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/src/Loonflow_logo.png
--------------------------------------------------------------------------------
/frontend/src/components/Setting/Notification/index.tsx:
--------------------------------------------------------------------------------
1 | export { NotificationList } from './NotificationList';
2 |
3 |
--------------------------------------------------------------------------------
/frontend/src/components/commonComponents/Snackbar/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as TopSnackbar } from './GlobalSnackbar';
--------------------------------------------------------------------------------
/static/images/donation_code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/static/images/donation_code.png
--------------------------------------------------------------------------------
/frontend/public/loonflow_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/loonflow_logo.png
--------------------------------------------------------------------------------
/frontend/public/loonflow_logo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/loonflow_logo1.png
--------------------------------------------------------------------------------
/backend/static/images/tenant0001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/backend/static/images/tenant0001.png
--------------------------------------------------------------------------------
/frontend/.env:
--------------------------------------------------------------------------------
1 | # 禁用源码映射,减少构建内存使用
2 | GENERATE_SOURCEMAP=false
3 |
4 | # 可选:提高 Node.js 内存限制
5 | NODE_OPTIONS=--max_old_space_size=6144
6 |
--------------------------------------------------------------------------------
/sphinx_docs/source/images/tenant_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/tenant_info.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/allow_withdraw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/allow_withdraw.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/donation_code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/donation_code.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/new_workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/new_workflow.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/title_property.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/title_property.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/category_tickets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/category_tickets.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/fill_ticket_form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_ticket_form.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/release_workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/release_workflow.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/role_management.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/role_management.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/select_workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/select_workflow.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/workflow_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/workflow_diagram.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/ticket_detail_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/ticket_detail_page.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/voluntary_pick_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/voluntary_pick_up.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/use_the_new_workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/use_the_new_workflow.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/user_dept_management.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/user_dept_management.png
--------------------------------------------------------------------------------
/backend/loonflow/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, unicode_literals
2 |
3 | from tasks import app as celery_app
4 |
5 | __all__ = ['celery_app']
--------------------------------------------------------------------------------
/sphinx_docs/source/api_reference/postman.rst:
--------------------------------------------------------------------------------
1 | Postman Collection
2 | ====================
3 |
4 |
5 | https://documenter.getpostman.com/view/15031929/2sB3WyJbap
--------------------------------------------------------------------------------
/sphinx_docs/source/images/application_management.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/application_management.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/fill_workflow_basic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_basic_info.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/notification_management.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/notification_management.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/workbench_duty_tickets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/workbench_duty_tickets.png
--------------------------------------------------------------------------------
/backend/apps/ticket/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TicketConfig(AppConfig):
5 | name = 'apps.ticket'
6 | verbose_name = '工单'
7 |
--------------------------------------------------------------------------------
/sphinx_docs/source/images/fill_workflow_form_design.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_form_design.png
--------------------------------------------------------------------------------
/backend/apps/account/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AccountConfig(AppConfig):
5 | name = 'apps.account'
6 | verbose_name = '账户'
7 |
--------------------------------------------------------------------------------
/sphinx_docs/source/images/workflow_version_management.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/workflow_version_management.png
--------------------------------------------------------------------------------
/backend/apps/workflow/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class WorkflowConfig(AppConfig):
5 | name = 'apps.workflow'
6 | verbose_name = '工作流'
7 |
--------------------------------------------------------------------------------
/sphinx_docs/source/images/fill_workflow_process_design01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_process_design01.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/fill_workflow_process_design02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_process_design02.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/fill_workflow_process_design03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_process_design03.png
--------------------------------------------------------------------------------
/sphinx_docs/source/images/start_node_title_auto_generate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/start_node_title_auto_generate.png
--------------------------------------------------------------------------------
/frontend/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | packageExtensions:
4 | "@types/node@*":
5 | peerDependenciesMeta:
6 | typescript:
7 | optional: true
8 |
--------------------------------------------------------------------------------
/sphinx_docs/source/others/release_notes/r1.x/index.rst:
--------------------------------------------------------------------------------
1 | r1.x Release Notes
2 | ====================
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 |
7 | r1.0.0
8 | r1.0.1+
9 |
--------------------------------------------------------------------------------
/sphinx_docs/source/others/release_notes/r2.x/index.rst:
--------------------------------------------------------------------------------
1 | r2.x Release Notes
2 | =====================
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 |
7 | r2.0.0
8 | r2.0.1+
9 |
--------------------------------------------------------------------------------
/sphinx_docs/source/others/release_notes/r3.x/index.rst:
--------------------------------------------------------------------------------
1 | r3.x Release Notes
2 | =====================
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 |
7 | r3.0.0
8 | r3.0.1+
9 |
--------------------------------------------------------------------------------
/backend/apps/util/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class UtilConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'apps.util'
7 |
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/organization/index.rst:
--------------------------------------------------------------------------------
1 | 3. Organization Management
2 | =================================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | user_dept
8 | role
9 |
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/setting/index.rst:
--------------------------------------------------------------------------------
1 | 4. Setting Management
2 | =================================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | application
8 | notification
9 | tenant
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/workflow/index.rst:
--------------------------------------------------------------------------------
1 | 2. Workflow Management
2 | =================================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | new_update_workflow
8 | workflow_version
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | patreon: blackholllcn
4 | custom: ['https://github.com/blackholll/loonflow/blob/master/README_zh.md#%E6%AC%A2%E8%BF%8E%E6%8D%90%E5%8A%A9']
--------------------------------------------------------------------------------
/backend/loonflow/contexts.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # 全局变量、常量
3 | #
4 | from django.conf import settings
5 |
6 |
7 | def global_variables(request):
8 | return {'VERSION': settings.VERSION}
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Ticket/AllTicket/index.tsx:
--------------------------------------------------------------------------------
1 | import TicketList from '../TicketList';
2 |
3 | function AllTicket() {
4 | return
5 | }
6 |
7 | export default AllTicket;
8 |
--------------------------------------------------------------------------------
/frontend/src/components/Ticket/OwnerTicket/index.tsx:
--------------------------------------------------------------------------------
1 | import TicketList from '../TicketList';
2 |
3 | function OwerTicket() {
4 | return
5 | }
6 |
7 | export default OwerTicket;
--------------------------------------------------------------------------------
/frontend/src/components/Ticket/ViewTicket/index.tsx:
--------------------------------------------------------------------------------
1 | import TicketList from '../TicketList';
2 |
3 | function ViewTicket() {
4 | return
5 | }
6 |
7 | export default ViewTicket;
--------------------------------------------------------------------------------
/frontend/src/components/Ticket/DutyTicket/index.tsx:
--------------------------------------------------------------------------------
1 | import TicketList from '../TicketList';
2 |
3 |
4 | function DutyTicket() {
5 | return
6 | }
7 |
8 | export default DutyTicket;
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/ticket/index.rst:
--------------------------------------------------------------------------------
1 | 1. Ticket Management
2 | =====================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | submit_ticket
8 | query_ticket
9 | handle_ticket
10 |
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/zh-CN/department.json:
--------------------------------------------------------------------------------
1 | {
2 | "parentDepartment": "上级部门",
3 | "parentDepartmentHelpText": "请在左侧部门树中选择上级部门",
4 | "departmentLeader": "部门负责人",
5 | "departmentApprover": "部门审批人"
6 | }
--------------------------------------------------------------------------------
/sphinx_docs/source/core_concepts/ticket_system_fundamentals/index.rst:
--------------------------------------------------------------------------------
1 | 1. Ticketing System Fundamentals
2 | ===================================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | ticket_workflow
8 |
--------------------------------------------------------------------------------
/frontend/src/components/Ticket/RelationTicket/index.tsx:
--------------------------------------------------------------------------------
1 | import TicketList from '../TicketList';
2 |
3 | function RelationTicket() {
4 | return
5 | }
6 |
7 | export default RelationTicket;
--------------------------------------------------------------------------------
/backend/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | include =
4 | apps/*
5 | services/*
6 | omit =
7 | apps/manage/*
8 |
9 | [report]
10 | skip_covered = True
11 |
12 | [html]
13 | directory = coverage_html_report
--------------------------------------------------------------------------------
/frontend/src/components/Ticket/InterveneTicket/index.tsx:
--------------------------------------------------------------------------------
1 | import TicketList from '../TicketList';
2 |
3 | function InterveneTicket() {
4 | return
5 | }
6 |
7 | export default InterveneTicket;
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | blank_issues_enabled: false
3 | contact_links:
4 | - about: 'Use the Loonflow Community Discord for questions & discussions'
5 | name: ❓ Questions
6 | url: 'https://discord.gg/dh8vBG6J'
7 |
--------------------------------------------------------------------------------
/sphinx_docs/source/introduction/welcome/index.rst:
--------------------------------------------------------------------------------
1 | 1. Welcome to LoonFlow
2 | ============================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | about_document
8 | about_loonflow
9 | getting_help
10 |
11 |
--------------------------------------------------------------------------------
/sphinx_docs/source/others/release_notes/r0.x/index.rst:
--------------------------------------------------------------------------------
1 | r0.x Release Notes
2 | =====================
3 |
4 | For detailed release notes of v0.x versions, please refer to GitHub Releases:
5 |
6 | https://github.com/blackholll/loonflow/releases
7 |
--------------------------------------------------------------------------------
/sphinx_docs/source/installtion_deployment/installation_guide/index.rst:
--------------------------------------------------------------------------------
1 | 2. Installation Guide
2 | ============================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | developer_setup
8 | deployment_with_docker_compose
9 |
10 |
--------------------------------------------------------------------------------
/sphinx_docs/source/installtion_deployment/system_requirements/index.rst:
--------------------------------------------------------------------------------
1 | 1. System Requirements
2 | ============================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | server_hardware_requirements
8 | os_environment_requirements
9 |
10 |
--------------------------------------------------------------------------------
/frontend/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/sphinx_docs/source/others/release_notes/index.rst:
--------------------------------------------------------------------------------
1 | This document contains release notes for all versions of Loonflow.
2 |
3 | .. toctree::
4 | :maxdepth: 3
5 | :caption: Version History
6 |
7 | r3.x/index
8 | r2.x/index
9 | r1.x/index
10 | r0.x/index
11 |
--------------------------------------------------------------------------------
/backend/service/csrf_service.py:
--------------------------------------------------------------------------------
1 | from django.utils.deprecation import MiddlewareMixin
2 |
3 |
4 | class DisableCSRF(MiddlewareMixin):
5 | def process_request(self, request):
6 | if request.path.startswith('/api/'):
7 | setattr(request, '_dont_enforce_csrf_checks', True)
8 |
--------------------------------------------------------------------------------
/frontend/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/en-US/department.json:
--------------------------------------------------------------------------------
1 | {
2 | "parentDepartment": "Parent Department",
3 | "parentDepartmentHelpText": "please select parent department on the left parent tree",
4 | "departmentLeader": "Department Leader",
5 | "departmentApprover": "Department Approver"
6 | }
--------------------------------------------------------------------------------
/backend/service/exception/custom_common_exception.py:
--------------------------------------------------------------------------------
1 | class CustomCommonException(Exception):
2 | """
3 | custom common exception. this type exception's message will display to user
4 | """
5 | def __init__(self, message=""):
6 | self.message = message
7 | super().__init__(self.message)
8 |
--------------------------------------------------------------------------------
/backend/apps/util/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from apps.loon_base_model import BaseModel
3 |
4 |
5 | # Create your models here.
6 | class Archive(BaseModel):
7 | """archived record"""
8 | model_name = models.CharField("model name", max_length=100)
9 | data = models.JSONField("archived data")
10 |
--------------------------------------------------------------------------------
/frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Workflow validation result
3 | */
4 | export interface WorkflowValidationResult {
5 | /** List of detected problems */
6 | problems: string[];
7 | /** Whether there are any problems */
8 | hasProblems: boolean;
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/services/authService.ts:
--------------------------------------------------------------------------------
1 | import apiClient from './api';
2 |
3 | export const login = async (email: string, password: string) => {
4 | try {
5 | const response = await apiClient.post('/api/v1.0/login', { email, password });
6 | return response.data;
7 | } catch (error) {
8 | throw error;
9 | }
10 | };
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/zh-CN/signIn.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Loonflow",
3 | "description": "企业级统一工作流解决方案",
4 | "emailLabel": "电子邮件地址",
5 | "passwordLabel": "密码",
6 | "rememberMe": "记住我",
7 | "signInButton": "登录",
8 | "forgotPassword": "忘记密码?",
9 | "noAccount": "还没有账号?注册",
10 | "welcomeBack": "欢迎回来!请登录继续。",
11 | "welcomeMessage": "欢迎!"
12 | }
--------------------------------------------------------------------------------
/frontend/src/types/readme.md:
--------------------------------------------------------------------------------
1 | # about
2 | This folder is for all interface definations
3 |
4 | # rule
5 | - use interface instend of type, except interface can not satisfy requirement
6 | - all startswith 'I'
7 | - if the interface is for api response, name should be contain res
8 | - if the interface is for api request, name should be contain req
9 | -
10 |
--------------------------------------------------------------------------------
/sphinx_docs/source/introduction/welcome/about_loonflow.rst:
--------------------------------------------------------------------------------
1 | 1.2 About Loonflow
2 | ============================
3 |
4 | Loonflow is an open-source process automation platform developed based on Django. After multiple versions of iteration and refactoring, we proudly present Loonflow 3.0 - a version that achieves a qualitative leap in visualization, flexibility, and scalability.
--------------------------------------------------------------------------------
/backend/apps/manage/templatetags/loonflow_filter.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.conf import settings
3 |
4 | register = template.Library()
5 |
6 |
7 | @register.filter()
8 | def add_version(_input):
9 | """
10 | 加上版本信息?v=xxx,主要是为了避免静态文件缓存
11 | :param _input:
12 | :return:
13 | """
14 | return '?v=%s' % settings.STATIC_FILES_VERSION
15 |
--------------------------------------------------------------------------------
/backend/service/format_response.py:
--------------------------------------------------------------------------------
1 | # import json
2 | from django.http import JsonResponse
3 |
4 | from django.http import HttpResponse
5 |
6 |
7 | def api_response(code, msg='', data=''):
8 | """
9 | 格式化返回
10 | :param code:
11 | :param msg:
12 | :param data:
13 | :return:
14 | """
15 | return JsonResponse(dict(code=code, data=data, msg=msg))
16 |
--------------------------------------------------------------------------------
/frontend/src/components/formFields/WorkflowInfoField.tsx:
--------------------------------------------------------------------------------
1 | import { Typography } from '@mui/material';
2 |
3 | interface WorkflowInfoFieldProps {
4 | value: any;
5 | }
6 |
7 | export default function WorkflowInfoField({ value }: WorkflowInfoFieldProps) {
8 | return (
9 | {value?.name ? value.name : ''}
10 | )
11 | }
--------------------------------------------------------------------------------
/backend/apps/loon_model_base_admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 |
4 | class ModelBaseAdmin(admin.ModelAdmin):
5 | list_display = ('creator', 'is_deleted', 'gmt_created', 'gmt_modified')
6 | readonly_fields = ['creator']
7 |
8 | def save_model(self, request, obj, form, change):
9 | if not obj.creator:
10 | obj.creator = request.user.username
11 | obj.save()
12 |
--------------------------------------------------------------------------------
/sphinx_docs/source/introduction/who_need_read.rst:
--------------------------------------------------------------------------------
1 | 2. Who Need to Read This Guide
2 | ================================
3 |
4 | This guide is intended for:
5 |
6 | - **End Users** - Who submit and handle tickets in their daily work
7 | - **Admins** - Who are responsible for deploying, customizing, and maintaining the LoonFlow instance
8 | - **Workflow Admins** - Who are responsible for configuring workflows, Intervene tickets
9 |
--------------------------------------------------------------------------------
/frontend/src/components/home/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import Typography from '@mui/material/Typography';
2 |
3 | export default function Home() {
4 | return (
5 |
6 |
7 | Welcome to the Home Page
8 |
9 |
10 | This is the home page content.
11 |
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/backend/service/manage/overview_service.py:
--------------------------------------------------------------------------------
1 | from service.base_service import BaseService
2 |
3 |
4 | class OverviewService(BaseService):
5 | """
6 | 总览
7 | """
8 | def __init__(self):
9 | pass
10 |
11 | def get_new_ticket_type_count_statistics_info(self, start_time: str, end_time: str)->tuple:
12 | """
13 | 获取每种类型工单创建数量统计数据
14 | :return:
15 | """
16 | return '', ''
17 |
--------------------------------------------------------------------------------
/frontend/src/components/formFields/TicketNodesField.tsx:
--------------------------------------------------------------------------------
1 | import { Typography } from '@mui/material';
2 |
3 | interface TicketNodesFieldProps {
4 | value: any | null;
5 | }
6 |
7 | export default function TicketNodesField({ value }: TicketNodesFieldProps) {
8 | return (
9 | {value ? value.map((item: any) => item.name).join(',') : ''}
10 | )
11 | }
--------------------------------------------------------------------------------
/frontend/src/components/home2/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import Typography from '@mui/material/Typography';
2 |
3 | export default function Home2() {
4 | return (
5 |
6 |
7 | Welcome to the Home Page222222
8 |
9 |
10 | This is the home page content.2222222222
11 |
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/docker_compose_deploy/.env:
--------------------------------------------------------------------------------
1 | # please change below info, especially REDIS_PASSWORD and POSTGRES_PASSWORD and ADMIN_PASSWORD
2 | REDIS_PASSWORD=loonflow123
3 | REDIS_PORT=6381
4 | POSTGRES_DB=loonflow
5 | POSTGRES_USER=loonflow
6 | POSTGRES_PASSWORD=rALnSWb!G^X4
7 | POSTGRES_PORT=5433
8 | PG_DATA_VOLUME=loonflow-pg-data
9 |
10 | TENANT_NAME=loonapp
11 | TENANT_DOMAIN=loonapp.com
12 | ADMIN_NAME=admin
13 | ADMIN_EMAIL=admin@loonapp.com
14 | ADMIN_PASSWORD=123456
15 |
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/organization/role.rst:
--------------------------------------------------------------------------------
1 | Role Management
2 | ==================
3 |
4 | Loonflow supports managing roles so you can create, edit, delete, and maintain role members. Roles are primarily used when configuring workflows to specify handlers, allowing you to assign a role as the processor for a workflow node.
5 |
6 | .. figure:: ../../images/role_management.png
7 | :width: 100%
8 | :align: center
9 | :alt: role management
10 |
11 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useSnackbar.ts:
--------------------------------------------------------------------------------
1 | // src/hooks/useSnackbar.ts
2 | import { useContext } from 'react';
3 | import { SnackbarContext } from '../components/commonComponents/Snackbar/SnackbarProvider';
4 |
5 | const useSnackbar = () => {
6 | const context = useContext(SnackbarContext);
7 | if (context === undefined) {
8 | throw new Error('useSnackbar must be used within a SnackbarProvider');
9 | }
10 | return context;
11 | };
12 |
13 | export default useSnackbar;
--------------------------------------------------------------------------------
/backend/apps/archived_model.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from apps.loon_base_model import BaseModel
3 | from django.utils.translation import gettext_lazy as _
4 | from django.contrib.postgres.fields import JSONField
5 |
6 |
7 | class Archived(BaseModel):
8 | """archived record"""
9 | model_name = models.CharField("model name", max_length=100)
10 | data = JSONField(_('archived data'))
11 |
12 | class Meta:
13 | app_label = 'archived'
14 |
15 |
--------------------------------------------------------------------------------
/backend/loonflow/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for loonflow project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.config")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/backend/service/redis_pool.py:
--------------------------------------------------------------------------------
1 | import redis
2 | from django.conf import settings
3 |
4 |
5 | redis_host = settings.REDIS_HOST
6 | redis_db = settings.REDIS_DB
7 | redis_port = settings.REDIS_PORT
8 | redis_password = settings.REDIS_PASSWORD
9 | if redis_password:
10 | POOL = redis.ConnectionPool(host=redis_host, port=redis_port, password=redis_password, max_connections=1000)
11 | else:
12 | POOL = redis.ConnectionPool(host=redis_host, port=redis_port, max_connections=1000)
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/components/formFields/TicketCurrentAssigneeInfosField.tsx:
--------------------------------------------------------------------------------
1 | import { Typography } from '@mui/material';
2 |
3 | interface TicketCurrentAssigneeInfosFieldProps {
4 | value: any;
5 | }
6 |
7 | export default function TicketCurrentAssigneeInfosField({ value }: TicketCurrentAssigneeInfosFieldProps) {
8 | return (
9 | {value ? value.map((item: any) => item.assignee).join(',') : ''}
10 | )
11 | }
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/en-US/signIn.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Loonflow",
3 | "description": "Unified Workflow Solutions for the Enterprise",
4 | "emailLabel": "Email Address",
5 | "passwordLabel": "Password",
6 | "rememberMe": "Remember me",
7 | "signInButton": "Sign In",
8 | "forgotPassword": "Forgot password?",
9 | "noAccount": "Don't have an account? Sign Up",
10 | "welcomeBack": "Welcome back! Please sign in to continue.",
11 | "welcomeMessage": "Welcome to our app!"
12 | }
--------------------------------------------------------------------------------
/frontend/src/types/common.ts:
--------------------------------------------------------------------------------
1 | export interface IApiResponse {
2 | code: 0;
3 | data: T;
4 | msg: string;
5 | }
6 |
7 | export interface IApiErrResponse {
8 | code: -1;
9 | msg: string;
10 | }
11 |
12 | export interface ISimpleEntity {
13 | id: string,
14 | name: string
15 | }
16 |
17 | export interface ISimpleQueryParam {
18 | page?: number;
19 | perPage?: number;
20 | searchKey?: string
21 |
22 | }
23 |
24 | export interface ILabel {
25 | [key: string]: any
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/src/components/formFields/TicketCreateAtField.tsx:
--------------------------------------------------------------------------------
1 | import { Typography } from '@mui/material';
2 | import { formatDate } from '../../utils/dateFormat';
3 |
4 |
5 |
6 | interface TicketCreatorFieldProps {
7 | value: string | null;
8 | }
9 |
10 | export default function TicketCreatedAtField({ value }: TicketCreatorFieldProps) {
11 | return (
12 | {value ? formatDate(value) : ''}
13 | )
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/backend/apps/manage/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from apps.manage.views import index, CommonConfigView, NotificationView, SimpleNotificationView, NotificationDetailView
3 |
4 | urlpatterns = [
5 | path("", index),
6 | path("/common", CommonConfigView.as_view()),
7 | path("/notifications", NotificationView.as_view()),
8 | path("/simple_notifications", SimpleNotificationView.as_view()),
9 | path("/notifications/", NotificationDetailView.as_view())
10 | ]
11 |
--------------------------------------------------------------------------------
/frontend/src/components/formFields/TicketActStateField.tsx:
--------------------------------------------------------------------------------
1 | import { Typography } from '@mui/material';
2 | import { useTranslation } from 'react-i18next';
3 |
4 | interface TicketActStateFieldProps {
5 | value: string;
6 | }
7 |
8 | export default function TicketActStateField({ value }: TicketActStateFieldProps) {
9 | const { t } = useTranslation();
10 |
11 | return (
12 | {t(`ticket.state.${value}`)}
13 | )
14 | }
--------------------------------------------------------------------------------
/frontend/src/components/formFields/TicketCreatorField.tsx:
--------------------------------------------------------------------------------
1 | import { Typography, Tooltip } from '@mui/material';
2 |
3 |
4 | interface TicketCreatorFieldProps {
5 | value: any | null;
6 | }
7 |
8 | export default function TicketCreatorField({ value }: TicketCreatorFieldProps) {
9 | return (
10 | {value ? `${value.name}(${value.alias})` : ''}
11 | )
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/backend/apps/homepage_view.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponse
2 | from django.views import View
3 |
4 |
5 | class HomepageView(View):
6 | def get(self, request, *args, **kwargs):
7 | """
8 | 首页
9 | :param request:
10 | :param args:
11 | :param kwargs:
12 | :return:
13 | """
14 | return HttpResponse('')
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/zh-CN/menu.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench": "工作台",
3 | "ticketManagement": "工单管理",
4 | "ticketDuty": "我的待办",
5 | "ticketOwner": "我的申请",
6 | "ticketRelation": "我的关联",
7 | "ticketView": "工单查看",
8 | "ticketIntervene": "工单干预",
9 | "ticketAll": "所有工单",
10 | "workflowManagement": "工作流管理",
11 | "organization": "组织架构",
12 | "userAndDept": "用户及部门",
13 | "role": "角色",
14 | "setting": "系统设置",
15 | "tenant": "租户管理",
16 | "application": "应用管理",
17 | "notification": "通知管理",
18 | "signin": "登录"
19 | }
--------------------------------------------------------------------------------
/frontend/src/types/notification.ts:
--------------------------------------------------------------------------------
1 | import { IApiResponse } from './common';
2 |
3 |
4 | export interface INotificationResEntity {
5 | id: string;
6 | label: string;
7 | createdAt: string;
8 | updatedAt: string;
9 | name: string;
10 | description: string;
11 | type: string;
12 | extra: any;
13 | tenantId: string;
14 | }
15 |
16 | export interface INotificationListResData {
17 | applicationInfoList: INotificationResEntity[]
18 | }
19 |
20 | export interface INotificationListRes extends IApiResponse { }
--------------------------------------------------------------------------------
/backend/service/util/link_service.py:
--------------------------------------------------------------------------------
1 | class Node:
2 | def __init__(self, data):
3 | self.data = data
4 | self.next = None
5 |
6 |
7 | class LinkedList:
8 | def __int__(self):
9 | self.head = None
10 |
11 | def append(self, data):
12 | new_node = Node(data)
13 | if not self.head:
14 | self.head = new_node
15 | else:
16 | current = self.head
17 | while current.next:
18 | current = current.next
19 | current.next = new_node
20 |
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/zh-CN/layout.json:
--------------------------------------------------------------------------------
1 | {
2 | "userInfo": "用户信息",
3 | "updateProfileSuccess": "更新用户信息成功",
4 | "updateProfileFail": "更新用户信息失败",
5 | "name": "姓名",
6 | "changePassword": "修改密码",
7 | "currentPassword": "当前密码",
8 | "newPassword": "新密码",
9 | "confirmPassword": "确认新密码",
10 | "changePasswordSuccess": "密码修改成功",
11 | "changePasswordFail": "密码修改失败",
12 | "passwordsNotMatch": "两次输入的新密码不一致",
13 | "currentPasswordRequired": "请输入当前密码",
14 | "newPasswordRequired": "请输入新密码",
15 | "confirmPasswordRequired": "请确认新密码"
16 | }
--------------------------------------------------------------------------------
/frontend/src/types/role.ts:
--------------------------------------------------------------------------------
1 | import { IApiResponse } from "./common";
2 |
3 | export interface IRoleResEntity {
4 | id: string;
5 | label: string;
6 | createdAt: string;
7 | updatedAt: string;
8 | name: string;
9 | description: string;
10 | tenantId: string;
11 | }
12 |
13 | export interface ISimpleRole {
14 | id: string;
15 | name: string;
16 | }
17 |
18 | export interface IRoleListResData {
19 | roleList: IRoleResEntity[]
20 | }
21 |
22 | export interface IRoleListRes extends IApiResponse { }
--------------------------------------------------------------------------------
/frontend/src/components/Ticket/TicketDetailPage/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import TicketDetail from '../TicketDetail';
4 |
5 | function TicketDetailPage() {
6 | const { ticketId } = useParams();
7 | const [refreshToken, setRefreshToken] = useState(0);
8 | console.log('ticketId', ticketId)
9 |
10 |
11 | return { setRefreshToken(refreshToken + 1) }} />
12 | }
13 |
14 | export default TicketDetailPage;
--------------------------------------------------------------------------------
/sphinx_docs/source/introduction/welcome/getting_help.rst:
--------------------------------------------------------------------------------
1 | 1.3 Get Help & Community Support
2 | =================================
3 |
4 | - 📝 GitHub Issues - Submit bug reports and feature requests. https://github.com/blackholll/loonflow/issues
5 | - 💬 Discussion Forum Discord. https://discord.gg/WuppaG638k
6 | - 📧 Commercial Support & Customization: For enterprise-level deep customization, technical training, or deployment support needs, please contact me at [blackholll@163.com;blackholll.cn@gmail.com].
7 | - 💰 Member Benefits. See https://patreon.com/blackholllcn for more details.
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/zh-CN/role.json:
--------------------------------------------------------------------------------
1 | {
2 | "roleList": "角色列表",
3 | "addRoleSuccess": "添加角色成功",
4 | "addRoleFail": "添加角色失败",
5 | "updateRoleSuccess": "更新角色成功",
6 | "updateRoleFail": "更新角色失败",
7 | "deleteRoleSuccess": "删除角色成功",
8 | "deleteRoleFail": "删除角色失败",
9 | "roleName": "角色名称",
10 | "roleDescription": "角色描述",
11 | "rolePermission": "角色权限",
12 | "roleMember": "角色成员",
13 | "deleteMemberConfirm": "确认删除成员",
14 | "deleteMemberConfirmMsg": "确定要从角色中删除该成员吗?删除后该成员将失去此角色的所有权限。",
15 | "deleteMemberSuccess": "删除成员成功",
16 | "deleteMemberFail": "删除成员失败"
17 | }
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/zh-CN/user.json:
--------------------------------------------------------------------------------
1 | {
2 | "leader": "负责人",
3 | "changeParentDept": "修改父部门",
4 | "passwordNotMatch": "两次输入的密码不一致",
5 | "resetPassword": "重置密码",
6 | "confirmResetPassword": "确认重置密码",
7 | "confirmResetPasswordMsg": "密码将被重置为'123456', 请尽快修改密码",
8 | "resetPasswordSuccess": "重置密码成功",
9 | "resetPasswordFail": "重置密码失败",
10 | "deleteUserSuccess": "删除用户成功",
11 | "deleteUserFail": "删除用户失败",
12 | "admin": "管理员",
13 | "workflowAdmin": "工作流管理员",
14 | "normal": "普通用户",
15 | "userDeptHelpText": "用户所属部门,支持设置多个部门,其中第一个部门将作为用户的主要部门,审批时将以主要部门作为用户部门来获取部门审批人"
16 | }
--------------------------------------------------------------------------------
/sphinx_docs/source/others/release_notes/r2.x/r2.0.0.rst:
--------------------------------------------------------------------------------
1 | r2.0.0
2 | ---------
3 |
4 | - Built-in ticket creation, viewing, processing, and management interface (major change in this release)
5 | - Support for users belonging to multiple departments simultaneously (significant change in this release)
6 | - flowlog API supports specifying order or reverse order
7 | - Workflow configuration interface supports viewing daily new ticket statistics
8 | - Ticket details support administrator intervention
9 | - Support for users to change their own passwords
10 | - Various other optimizations
11 |
--------------------------------------------------------------------------------
/backend/service/account/account_auth_service.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.backends import ModelBackend
2 | from django.contrib.auth import get_user_model
3 |
4 |
5 | class EmailBackend(ModelBackend):
6 | def authenticate(self, request, email=None, password=None, **kwargs):
7 | UserModel = get_user_model()
8 | try:
9 | user = UserModel.objects.get(email=email)
10 | except UserModel.DoesNotExist:
11 | return None
12 | else:
13 | if user.check_password(password):
14 | return user
15 | return None
16 |
--------------------------------------------------------------------------------
/backend/service/common/log_service.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import logging
3 | import traceback
4 |
5 | logger = logging.getLogger('django')
6 |
7 |
8 | def auto_log(func):
9 | """
10 | auto write log decorator
11 | :param func:
12 | :return:
13 | """
14 | @functools.wraps(func)
15 | def _deco(*args, **kwargs):
16 | try:
17 | real_func = func(*args, **kwargs)
18 | return real_func
19 | except Exception as e:
20 | logger.error(traceback.format_exc())
21 | return False, e.__str__()
22 | return _deco
23 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/utils/PrivateRoute.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Navigate } from 'react-router-dom';
3 | import { useSelector } from 'react-redux';
4 | import { RootState } from '../store';
5 |
6 | interface PrivateRouteProps {
7 | element: React.ReactElement | null;
8 | }
9 |
10 | const PrivateRoute: React.FC = ({ element }) => {
11 | const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated);
12 | console.log('routeisAuthenticated:', isAuthenticated);
13 | return isAuthenticated ? element : ;
14 | };
15 |
16 | export default PrivateRoute;
17 |
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/en-US/menu.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench": "Workbench",
3 | "ticketManagement": "Ticket Management",
4 | "ticketDuty": "My Duty",
5 | "ticketOwner": "My Owner",
6 | "ticketRelation": "My Relation",
7 | "ticketView": "My View",
8 | "ticketIntervene": "My Intervene",
9 | "ticketAll": "All tickets",
10 | "workflowManagement": "Workflow Management",
11 | "organization": "Organization",
12 | "userAndDept": "User & Dept",
13 | "role": "Role",
14 | "setting": "Setting",
15 | "tenant": "Tenant",
16 | "application": "Application",
17 | "notification": "Notification",
18 | "signin": "Sign In"
19 | }
--------------------------------------------------------------------------------
/docker_compose_deploy/loonflow-ui/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22.18.0-slim AS builder
2 |
3 | RUN corepack enable && corepack prepare yarn@4.4.1 --activate
4 |
5 | COPY ./frontend /app/loonflow/
6 | WORKDIR /app/loonflow
7 | ENV BROWSERSLIST_IGNORE_OLD_DATA=true
8 |
9 | RUN yarn install --immutable && yarn build
10 |
11 |
12 | FROM nginx:1.24.0-alpine
13 | LABEL maintainer=blackholll@163.com
14 |
15 | COPY docker_compose_deploy/loonflow-ui/nginx.conf /etc/nginx/nginx.conf
16 |
17 | COPY --from=builder /app/loonflow/build/ /usr/share/nginx/html/
18 |
19 | RUN mkdir /var/log/loonflow/
20 | EXPOSE 80
21 |
22 | CMD ["nginx", "-g", "daemon off;"]
23 |
--------------------------------------------------------------------------------
/frontend/src/types/dept.ts:
--------------------------------------------------------------------------------
1 | import { ISimpleUser } from "./user";
2 |
3 | export interface ISimpleDept {
4 | id: string;
5 | name: string;
6 | children: ISimpleDept[];
7 | }
8 |
9 | export interface ISimpleDeptPath {
10 | id: string;
11 | name: string;
12 | path: string;
13 | }
14 |
15 | export interface ISimpleParentDept {
16 | id: string;
17 | name: string;
18 | }
19 |
20 | export interface IDept {
21 | id: string;
22 | name: string;
23 | label: any;
24 | creatorInfo: ISimpleUser,
25 | leaderInfo: ISimpleUser,
26 | approverInfoList: ISimpleUser[],
27 | children: IDept,
28 | parentDeptInfo: ISimpleParentDept,
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/sphinx_docs/README.md:
--------------------------------------------------------------------------------
1 | # loonflow document
2 | This is mult-language document, default language is English.
3 |
4 | ## quick start
5 |
6 | ### 1. install dependencies
7 | ```bash
8 | cd sphinx_doc
9 | pip install -r requirements.txt
10 | ```
11 |
12 | ### 2. build html
13 | ```bash
14 |
15 | # get latest pot files
16 | make gettext
17 |
18 | # gen po files
19 | make update-translations
20 |
21 | # update po files
22 |
23 | # build English document
24 | make html
25 | # build Chinese document
26 | make html-zh
27 |
28 | # review document
29 | # English: open _build/html/index.html in browser
30 | # Chinese: open _build/html-zh/index.html in browser
31 | ```
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/sphinx_docs/requirements.txt:
--------------------------------------------------------------------------------
1 | alabaster==1.0.0
2 | babel==2.17.0
3 | certifi==2025.11.12
4 | charset-normalizer==3.4.4
5 | click==8.3.1
6 | docutils==0.21.2
7 | idna==3.11
8 | imagesize==1.4.1
9 | jieba==0.42.1
10 | Jinja2==3.1.6
11 | MarkupSafe==3.0.3
12 | packaging==25.0
13 | Pygments==2.19.2
14 | requests==2.32.5
15 | roman-numerals-py==3.1.0
16 | snowballstemmer==3.0.1
17 | Sphinx==8.2.3
18 | sphinx-intl==2.3.2
19 | sphinx-rtd-theme==3.0.2
20 | sphinxcontrib-applehelp==2.0.0
21 | sphinxcontrib-devhelp==2.0.0
22 | sphinxcontrib-htmlhelp==2.1.0
23 | sphinxcontrib-jquery==4.1
24 | sphinxcontrib-jsmath==1.0.1
25 | sphinxcontrib-qthelp==2.0.0
26 | sphinxcontrib-serializinghtml==2.0.0
27 | urllib3==2.5.0
--------------------------------------------------------------------------------
/backend/service/common/schema_valid_service.py:
--------------------------------------------------------------------------------
1 | from service.base_service import BaseService
2 | from schema import SchemaUnexpectedTypeError
3 |
4 |
5 | class SchemaValidService(BaseService):
6 |
7 | @staticmethod
8 | def parse_integer_list(value):
9 | print("11111")
10 | if value is None:
11 | return
12 | else:
13 | try:
14 | return [int(item if item != "" else 0) for item in value]
15 | except Exception:
16 | raise MyCustomError("11111")
17 |
18 | @staticmethod
19 | def parse_str_list(value):
20 | return [str(item) for item in value]
21 |
22 |
23 |
24 | class MyCustomError(Exception):
25 | pass
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/setting/application.rst:
--------------------------------------------------------------------------------
1 | Application Management
2 | ======================
3 |
4 | Applications are used to authenticate API calls. After creating an application, a token is generated and should be used to sign each request. The signing algorithm is documented in the Postman collection: https://documenter.getpostman.com/view/15031929/2sB3WyJbap
5 |
6 | .. figure:: ../../images/application_management.png
7 | :width: 100%
8 | :align: center
9 | :alt: Application management page
10 |
11 |
12 | Application types
13 | -------------------
14 | - admin: Administrators can call all APIs.
15 | - workflow_admin: Workflow administrators can call workflow/ticket APIs and common APIs.
16 |
--------------------------------------------------------------------------------
/frontend/src/components/formFields/types.ts:
--------------------------------------------------------------------------------
1 | import { IFormField } from '../../types/workflowDesign';
2 |
3 | // 基础字段组件属性接口
4 | export interface BaseFieldProps {
5 | field: IFormField;
6 | value?: any;
7 | onChange?: (value: any) => void;
8 | disabled?: boolean;
9 | readOnly?: boolean;
10 | required?: boolean;
11 | error?: boolean;
12 | helperText?: string;
13 | size?: 'small' | 'medium' | 'large';
14 | variant?: 'outlined' | 'filled' | 'standard';
15 | fullWidth?: boolean;
16 | }
17 |
18 |
19 | // 字段组件配置
20 | export interface FieldComponentConfig {
21 | // mode: FieldRenderMode;
22 | showLabel?: boolean;
23 | showDescription?: boolean;
24 | showValidation?: boolean;
25 | }
--------------------------------------------------------------------------------
/frontend/src/services/tenant.ts:
--------------------------------------------------------------------------------
1 | import apiClient from "./api";
2 | import {ITenantDetailRes} from "../types/tenant";
3 |
4 | export const getTenantDetail = async (tenantId: string): Promise => {
5 | try {
6 | const response = await apiClient.get(`/api/v1.0/accounts/tenants/${tenantId}`);
7 | return response.data;
8 | } catch (error) {
9 | throw error;
10 | }
11 | }
12 |
13 | export const getTenantByDomain = async (domain: string): Promise => {
14 | try {
15 | const response = await apiClient.get('/api/v1.0/accounts/tenants/by_domain', {
16 | params: { domain }
17 | });
18 | return response.data;
19 | } catch (error) {
20 | throw error;
21 | }
22 | };
--------------------------------------------------------------------------------
/backend/apps/manage/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from apps.loon_base_model import BaseCommonModel
3 |
4 |
5 | class Notification(BaseCommonModel):
6 | """
7 | notice config, need encrypt
8 | """
9 | NOTICE_TYPE_CHOICE = [
10 | ("dingtalk", "dingtalk"),
11 | ("wecom", "wecom"),
12 | ("feishu", "feishu"),
13 | ("hook", "hook")
14 | ]
15 | name = models.CharField("name", max_length=50, null=False, default="")
16 | description = models.CharField("description", max_length=200, null=False, default="")
17 | type = models.CharField("type", max_length=50, null=False, default="")
18 | extra = models.JSONField("extra", max_length=1000, null=False)
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/en-US/layout.json:
--------------------------------------------------------------------------------
1 | {
2 | "userInfo": "User Info",
3 | "updateProfileSuccess": "Update Profile Success",
4 | "updateProfileFail": "Update Profile Fail",
5 | "name": "Name",
6 | "changePassword": "Change Password",
7 | "currentPassword": "Current Password",
8 | "newPassword": "New Password",
9 | "confirmPassword": "Confirm New Password",
10 | "changePasswordSuccess": "Password changed successfully",
11 | "changePasswordFail": "Failed to change password",
12 | "passwordsNotMatch": "New passwords do not match",
13 | "currentPasswordRequired": "Please enter current password",
14 | "newPasswordRequired": "Please enter new password",
15 | "confirmPasswordRequired": "Please confirm new password"
16 | }
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/zh-CN/setting.json:
--------------------------------------------------------------------------------
1 | {
2 | "tenant": {
3 | "tenantInfo": "租户信息",
4 | "tenantDetail": "租户详情",
5 | "name": "名称",
6 | "domain": "域名",
7 | "workflowLimit": "工作流限制",
8 | "ticketLimit": "工单限制",
9 | "unLimited": "无限制",
10 | "tenantId": "租户ID"
11 | },
12 | "application": {
13 | "appTypeDescription": "管理员可以调用所有api,工作流管理员只能调用工单和工作流相关api和一些通用api",
14 | "applicationList": "应用列表",
15 | "searchWithKeyword": "关键字搜索",
16 | "applicationDetial": "应用详情",
17 | "newApplication": "新建应用",
18 | "tokenCopied": "Token已复制"
19 | },
20 | "notification": {
21 | "notificationList": "通知列表",
22 | "notificationDetial": "通知详情",
23 | "newNotification": "新建通知",
24 | "notificationTypeDescription": "通知类型说明"
25 | }
26 | }
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/en-US/role.json:
--------------------------------------------------------------------------------
1 | {
2 | "roleList": "Role List",
3 | "addRoleSuccess": "Add Role Success",
4 | "addRoleFail": "Add Role Fail",
5 | "updateRoleSuccess": "Update Role Success",
6 | "updateRoleFail": "Update Role Fail",
7 | "deleteRoleSuccess": "Delete Role Success",
8 | "deleteRoleFail": "Delete Role Fail",
9 | "roleName": "Role Name",
10 | "roleDescription": "Role Description",
11 | "rolePermission": "Role Permission",
12 | "roleMember": "Role Member",
13 | "deleteMemberConfirm": "Confirm Delete Member",
14 | "deleteMemberConfirmMsg": "Are you sure you want to remove this member from the role? The member will lose all permissions associated with this role.",
15 | "deleteMemberSuccess": "Delete Member Success",
16 | "deleteMemberFail": "Delete Member Fail"
17 | }
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/workflow/workflow_version.rst:
--------------------------------------------------------------------------------
1 | Workflow Version Management
2 | ===========================
3 |
4 | Loonflow supports multiple workflow versions. Each workflow can have the
5 | following version types:
6 |
7 | - **default**: The active version used when a user selects the workflow in
8 | Workbench to create a new ticket.
9 | - **candidate**: A draft version for validation. Administrators can switch the
10 | version type or create test tickets with the candidate version.
11 | - **archived**: A retired version. New tickets cannot be created with it, but
12 | existing tickets created in the past can still be processed.
13 |
14 | .. figure:: ../../images/workflow_version_management.png
15 | :width: 100%
16 | :align: center
17 | :alt: workflow version management
18 |
--------------------------------------------------------------------------------
/backend/requirements/common.txt:
--------------------------------------------------------------------------------
1 | #requrements/common.txt
2 | #python=3.12.x , you must use python3.12.x
3 | amqp==5.3.1
4 | asgiref==3.10.0
5 | billiard==4.2.2
6 | celery==5.5.3
7 | certifi==2025.10.5
8 | cffi==2.0.0
9 | charset-normalizer==3.4.3
10 | click==8.3.0
11 | click-didyoumean==0.3.1
12 | click-plugins==1.1.1.2
13 | click-repl==0.3.0
14 | cryptography==46.0.2
15 | Django==5.2.7
16 | idna==3.10
17 | kombu==5.5.4
18 | packaging==25.0
19 | prompt_toolkit==3.0.52
20 | psycopg==3.2.10
21 | psycopg-binary==3.2.10
22 | pycparser==2.23
23 | PyJWT==2.10.1
24 | python-dateutil==2.9.0.post0
25 | redis==6.4.0
26 | requests==2.32.5
27 | schema==0.7.7
28 | simplejson==3.20.2
29 | six==1.17.0
30 | sqlparse==0.5.3
31 | typing_extensions==4.15.0
32 | tzdata==2025.2
33 | urllib3==2.5.0
34 | vine==5.1.0
35 | wcwidth==0.2.14
36 |
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/ticket/submit_ticket.rst:
--------------------------------------------------------------------------------
1 | submit ticket
2 | ===============
3 |
4 |
5 | This guide is intended for users who want to submit a ticket.
6 |
7 | 1. Login to LoonFlow
8 | 2. Clieck "Workbench" in the left sidebar
9 | 3. Select a workflow from the dropdown menu
10 |
11 | .. figure:: ../../images/select_workflow.png
12 | :width: 100%
13 | :align: center
14 | :alt: Select Workflow
15 |
16 | 4. Click the "New Ticket" button
17 | 5. Fill in the ticket information
18 |
19 | .. figure:: ../../images/fill_ticket_form.png
20 | :width: 100%
21 | :align: center
22 | :alt: Fill Ticket Info
23 |
24 | 6. Click corresponding button listed in the bottom of the dialogpage to submit the ticket.
25 | 7. The ticket will be submitted and the next handle user will receive a notification.
26 |
--------------------------------------------------------------------------------
/frontend/src/utils/jwt.ts:
--------------------------------------------------------------------------------
1 |
2 | export const parseJwt = (token: string): any => {
3 | try {
4 | const base64Url = token.split('.')[1];
5 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
6 | const jsonPayload = decodeURIComponent(
7 | atob(base64)
8 | .split('')
9 | .map(function (c) {
10 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
11 | })
12 | .join('')
13 | );
14 |
15 | return JSON.parse(jsonPayload);
16 | } catch (e) {
17 | return null;
18 | }
19 | };
20 |
21 | export const getJwtExpiration = (token: string): Date | null => {
22 | const decoded = parseJwt(token);
23 | if (!decoded || !decoded.exp) {
24 | return null;
25 | }
26 | return new Date(decoded.exp * 1000); // JWT exp is in seconds, convert to milliseconds
27 | };
--------------------------------------------------------------------------------
/frontend/src/types/tenant.ts:
--------------------------------------------------------------------------------
1 | import { IApiResponse } from './common';
2 |
3 |
4 | export interface ITenantDetailResEntity {
5 | id: string;
6 | label: string;
7 | createdAt: string;
8 | updatedAt: string;
9 | name: string;
10 | domain: string;
11 | icon: string;
12 | workflowLimit: number;
13 | ticketLimit: number;
14 | isActive: boolean;
15 | }
16 |
17 | export interface ITenantBasicInfo {
18 | id: string;
19 | name: string;
20 | domain: string;
21 | icon: string
22 | }
23 |
24 | export interface ITenantDetailResData {
25 | tenantInfo: ITenantDetailResEntity
26 | }
27 |
28 | export interface ITenantBasicInfoResData {
29 | tenantInfo: ITenantBasicInfo
30 | }
31 |
32 |
33 | export interface ITenantDetailRes extends IApiResponse { }
34 |
35 | export interface ITenantBasicInfoRes extends IApiResponse { }
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/setting/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/user_guide/setting/index.rst:2
22 | msgid "4. Setting Management"
23 | msgstr "4. 配置管理"
24 |
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/ticket/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/user_guide/ticket/index.rst:2
22 | msgid "1. Ticket Management"
23 | msgstr "1. 工单管理"
24 |
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/workflow/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/user_guide/workflow/index.rst:2
22 | msgid "2. Workflow Management"
23 | msgstr "2. 流程管理"
24 |
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/introduction/welcome/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/introduction/welcome/index.rst:2
22 | msgid "1. Welcome to LoonFlow"
23 | msgstr "1. 欢迎使用 LoonFlow"
24 |
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r1.x/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/others/release_notes/r1.x/index.rst:2
22 | msgid "r1.x Release Notes"
23 | msgstr "r1.x 发布说明"
24 |
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r2.x/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/others/release_notes/r2.x/index.rst:2
22 | msgid "r2.x Release Notes"
23 | msgstr "r2.x 发布说明"
24 |
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r3.x/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/others/release_notes/r3.x/index.rst:2
22 | msgid "r3.x Release Notes"
23 | msgstr "r3.x 发布说明"
24 |
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/organization/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/user_guide/organization/index.rst:2
22 | msgid "3. Organization Management"
23 | msgstr "3. 组织管理"
24 |
25 |
--------------------------------------------------------------------------------
/backend/manage_test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.test")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError:
10 | # The above import may fail for some other reason. Ensure that the
11 | # issue is really that Django is missing to avoid masking other
12 | # exceptions on Python 2.
13 | try:
14 | import django
15 | except ImportError:
16 | raise ImportError(
17 | "Couldn't import Django. Are you sure it's installed and "
18 | "available on your PYTHONPATH environment variable? Did you "
19 | "forget to activate a virtual environment?"
20 | )
21 | raise
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/setting/tenant.rst:
--------------------------------------------------------------------------------
1 | Tenant Management
2 | ====================
3 |
4 | Loonflow supports multi-tenant. Most users only need a single tenant; if you require multi-tenant capabilities, please contact blackholll (blackholll@163.com; blackholll_cn@gmail.com) for licensing. In multi-tenant mode all data—users, tickets, workflows, notifications, applications, roles, departments, and more—are fully isolated. Typical scenarios include group enterprises or SaaS deployments where multiple independent organizations share one platform.
5 |
6 | .. figure:: ../../images/tenant_info.png
7 | :width: 100%
8 | :align: center
9 | :alt: tenant info
10 |
11 | Future roadmap
12 | --------------
13 | Planned: cross-tenant ticket collaboration, allowing tickets to flow between tenants. Example scenarios: inter-company ticket handling, external customer cases, supplier tickets, and reseller/partner support.
--------------------------------------------------------------------------------
/.github/workflows/unit_test.yml.disabled:
--------------------------------------------------------------------------------
1 | name: Unit test CI
2 |
3 | on:
4 | push:
5 | tags:
6 | - r**
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | max-parallel: 4
14 | matrix:
15 | python-version: [ "3.12" ]
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: actions/setup-python@v5
20 | with:
21 | python-version: ${{ matrix.python-version }}
22 | - name: Install Dependencies
23 | run: |
24 | python -m pip install --upgrade pip
25 | pip install -r requirements/test.txt
26 | - name: Create Database
27 | run: |
28 | python manage_test.py makemigrations
29 | python manage_test.py migrate
30 | - name: Run Unit Tests
31 | run: |
32 | coverage run manage_test.py test
33 | coverage report
34 |
35 |
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/en-US/user.json:
--------------------------------------------------------------------------------
1 | {
2 | "leader": "Leader",
3 | "changeParentDept": "Change Parent Dept",
4 | "passwordNotMatch": "The two passwords entered are not the same",
5 | "resetPassword": "Reset Password",
6 | "confirmResetPassword": "Confirm Reset Password",
7 | "confirmResetPasswordMsg": "Password will be reset to '123456', please change it as soon as possible",
8 | "resetPasswordSuccess": "Reset Password Success",
9 | "resetPasswordFail": "Reset Password Fail",
10 | "deleteUserSuccess": "Delete User Success",
11 | "deleteUserFail": "Delete User Fail",
12 | "admin": "Admin",
13 | "workflowAdmin": "Workflow Admin",
14 | "normal": "Normal",
15 | "userDeptHelpText": "User belongs to departments, support setting multiple departments, the first department will be used as the main department, and the department approvers will be obtained based on the main department when approving"
16 | }
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/installtion_deployment/installation_guide/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/installtion_deployment/installation_guide/index.rst:2
22 | msgid "2. Installation Guide"
23 | msgstr "2. 安装指南"
24 |
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/installtion_deployment/system_requirements/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/installtion_deployment/system_requirements/index.rst:2
22 | msgid "1. System Requirements"
23 | msgstr "1. 系统要求"
24 |
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/setting/notification.rst:
--------------------------------------------------------------------------------
1 | Notification Management
2 | =========================
3 |
4 | Loonflow lets you add, edit, and delete notifications. Multiple delivery
5 | channels are available so you can wire notifications into your own systems.
6 |
7 | .. figure:: ../../images/notification_management.png
8 | :width: 100%
9 | :align: center
10 | :alt: notification management
11 |
12 |
13 | Notification Types
14 | -------------------
15 |
16 | - hook: Triggers a POST request to your notification endpoint with the current
17 | ticket data in request body. The token is used for signing, following the same
18 | approach as application tokens.
19 | - teams (planned): Sends to a Teams bot.
20 | - email (planned): Sends email; requires mail server configuration.
21 | - dingtalk (planned): Sends via DingTalk bot.
22 | - wecom (planned): Sends via WeCom bot.
23 | - feishu (planned): Sends via Feishu bot.
24 |
--------------------------------------------------------------------------------
/frontend/src/store/tenantSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import { ITenantBasicInfo } from '../types/tenant';
3 |
4 | interface TenantState {
5 | tenantInfo: ITenantBasicInfo | null;
6 | lastUpdateTime: number | null;
7 | }
8 |
9 | const initialState: TenantState = {
10 | tenantInfo: null,
11 | lastUpdateTime: null,
12 | };
13 |
14 | const tenantSlice = createSlice({
15 | name: 'tenant',
16 | initialState,
17 | reducers: {
18 | setTenantBasicInfo: (state, action: PayloadAction) => {
19 | state.tenantInfo = action.payload;
20 | state.lastUpdateTime = Date.now();
21 | },
22 | clearTenantInfo: (state) => {
23 | state.tenantInfo = null;
24 | state.lastUpdateTime = null;
25 | },
26 | },
27 | });
28 |
29 | export const { setTenantBasicInfo, clearTenantInfo } = tenantSlice.actions;
30 | export const tenantReducer = tenantSlice.reducer;
--------------------------------------------------------------------------------
/backend/apps/ticket/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from apps.ticket.views import TicketListView,TicketDetailFormView, TicketDetailActionsView, TicketHandleView, TicketFlowHistoryView, TicketCurrentNodeInfosView, TicketDetailAdminActionsView, TicketMockExternalAssigneeView
3 |
4 | urlpatterns = [
5 | path('', TicketListView.as_view()),
6 | path('//ticket_detail_form', TicketDetailFormView.as_view()),
7 | path('//ticket_detail_actions', TicketDetailActionsView.as_view()),
8 | path('//ticket_detail_admin_actions', TicketDetailAdminActionsView.as_view()),
9 | path('//handle', TicketHandleView.as_view()),
10 | path('//ticket_flow_history', TicketFlowHistoryView.as_view()),
11 | path('//current_node_infos', TicketCurrentNodeInfosView.as_view()),
12 | path('/mock_external_assignee', TicketMockExternalAssigneeView.as_view()),
13 | ]
14 |
--------------------------------------------------------------------------------
/frontend/src/components/commonComponents/Snackbar/GlobalSnackbar.tsx:
--------------------------------------------------------------------------------
1 | // src/components/Snackbar/GlobalSnackbar.tsx
2 | import { Alert, Snackbar } from '@mui/material';
3 | import React from 'react';
4 |
5 | interface GlobalSnackbarProps {
6 | open: boolean;
7 | onClose: (event: React.SyntheticEvent | Event, reason?: string) => void;
8 | message: string | React.ReactNode;
9 | severity: 'error' | 'warning' | 'info' | 'success';
10 | }
11 |
12 | const GlobalSnackbar: React.FC = ({ open, onClose, message, severity }) => {
13 | return (
14 |
23 |
24 | {message}
25 |
26 |
27 | );
28 | };
29 |
30 | export default GlobalSnackbar;
--------------------------------------------------------------------------------
/frontend/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import { persistStore, persistReducer } from 'redux-persist';
3 | import storage from 'redux-persist/lib/storage';
4 | import { authReducer, loginState, logoutState } from './authSlice';
5 | import { tenantReducer, setTenantBasicInfo, clearTenantInfo } from './tenantSlice';
6 |
7 | const persistConfig = {
8 | key: 'root',
9 | storage,
10 | };
11 |
12 | const persistedAuthReducer = persistReducer(persistConfig, authReducer);
13 | const persistedTenantReducer = persistReducer(persistConfig, tenantReducer);
14 |
15 | const store = configureStore({
16 | reducer: {
17 | auth: persistedAuthReducer,
18 | tenant: persistedTenantReducer,
19 | },
20 | });
21 | export const persistor = persistStore(store);
22 |
23 | export type RootState = ReturnType;
24 |
25 | export { store, loginState, logoutState, setTenantBasicInfo, clearTenantInfo };
26 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/core_concepts/ticket_system_fundamentals/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: loonflow \n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language: zh_CN\n"
15 | "Language-Team: zh_CN \n"
16 | "Plural-Forms: nplurals=1; plural=0;\n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=utf-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 | "Generated-By: Babel 2.17.0\n"
21 |
22 | #: ../../source/core_concepts/ticket_system_fundamentals/index.rst:2
23 | msgid "1. Ticketing System Fundamentals"
24 | msgstr "工单系统基础概念"
25 |
26 |
--------------------------------------------------------------------------------
/backend/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 | import warnings
5 | warnings.filterwarnings("ignore")
6 | if __name__ == "__main__":
7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.config")
8 | try:
9 | from django.core.management import execute_from_command_line
10 | except ImportError:
11 | # The above import may fail for some other reason. Ensure that the
12 | # issue is really that Django is missing to avoid masking other
13 | # exceptions on Python 2.
14 | try:
15 | import django
16 | except ImportError:
17 | raise ImportError(
18 | "Couldn't import Django. Are you sure it's installed and "
19 | "available on your PYTHONPATH environment variable? Did you "
20 | "forget to activate a virtual environment?"
21 | )
22 | raise
23 | execute_from_command_line(sys.argv)
24 |
--------------------------------------------------------------------------------
/frontend/src/types/user.ts:
--------------------------------------------------------------------------------
1 | import { IApiResponse } from './common';
2 |
3 | export interface IUser {
4 | id: string;
5 | name: string;
6 | type: string;
7 | alias: string;
8 | email: string;
9 | tenantId: string;
10 | phone: string;
11 | avatar?: string;
12 | lang?: string;
13 | isActive: boolean;
14 | deptInfoList: any[]; // 使用any避免循环依赖,实际使用时应该导入ISimpleDept
15 | }
16 |
17 | export interface IUserListResData {
18 | page: number;
19 | perPage: number;
20 | total: number;
21 | userInfoList: IUser[]
22 | }
23 |
24 | export interface IUserSimpleListResData {
25 | page: number;
26 | perPage: number;
27 | total: number;
28 | userInfoList: ISimpleUser[]
29 | }
30 |
31 | export interface ISimpleUser {
32 | id: string;
33 | name: string;
34 | alias: string;
35 | email: string;
36 | }
37 |
38 | export interface ISimpleUserListRes extends IApiResponse { }
39 |
--------------------------------------------------------------------------------
/frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18n from '../../../../i18n';
2 |
3 | /**
4 | * Get translated validation message
5 | * @param key The translation key
6 | * @param params Optional parameters for interpolation
7 | * @returns Translated message
8 | */
9 | export const t = (key: string, params?: Record): string => {
10 | return i18n.t(key, params) as string;
11 | };
12 |
13 | /**
14 | * Get translated validation message for workflow validation
15 | * @param category The validation category (basic, connectivity, etc.)
16 | * @param messageKey The specific message key
17 | * @param params Optional parameters for interpolation
18 | * @returns Translated message
19 | */
20 | export const getValidationMessage = (
21 | category: string,
22 | messageKey: string,
23 | params?: Record
24 | ): string => {
25 | return t(`workflowValidation.${category}.${messageKey}`, params);
26 | };
27 |
--------------------------------------------------------------------------------
/sphinx_docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/backend/apps/workflow/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from apps.workflow.views import WorkflowView, WorkflowInitNodeView, WorkflowVersionsView, WorkflowDetailView, WorkflowTicketCreationFormView, WorkflowTicketCreationActionsView, WorkflowProcessSingleSchemaView, WorkflowVersionDetailView
4 |
5 | urlpatterns = [
6 | path('', WorkflowView.as_view()),
7 | path('/', WorkflowDetailView.as_view()),
8 | path('//init_node', WorkflowInitNodeView.as_view()),
9 | path('//versions', WorkflowVersionsView.as_view()),
10 | path('//versions/', WorkflowVersionDetailView.as_view()),
11 | path('//ticket_creation_form', WorkflowTicketCreationFormView.as_view()),
12 | path('//ticket_creation_actions', WorkflowTicketCreationActionsView.as_view()),
13 | path('//process_single_schema', WorkflowProcessSingleSchemaView.as_view()),
14 | ]
--------------------------------------------------------------------------------
/frontend/src/components/Organization/UserDept/index.tsx:
--------------------------------------------------------------------------------
1 | import { Tab, Tabs } from '@mui/material';
2 | import { useState } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 | import Dept from './Dept';
5 | import User from './User';
6 |
7 |
8 |
9 | function UserDept() {
10 | const [activeTab, setActiveTab] = useState('users');
11 | const { t } = useTranslation();
12 |
13 | return (
14 |
15 |
setActiveTab(newValue)}
18 | >
19 |
20 |
21 |
22 |
23 | {activeTab === 'users' ? (
24 |
25 | ) : (
26 |
27 | )}
28 |
29 | );
30 | }
31 |
32 | export default UserDept;
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/en-US/setting.json:
--------------------------------------------------------------------------------
1 | {
2 | "tenant": {
3 | "name": "Name",
4 | "domain": "Domain",
5 | "workflowLimit": "Workflow Limit",
6 | "ticketLimit": "Ticket Limit",
7 | "unLimited": "Unlimited",
8 | "tenantInfo": "Tenant Info",
9 | "tenantDetail": "Tenant Detail",
10 | "tenantId": "Tenant Id"
11 | },
12 | "application": {
13 | "appTypeDescription": "Admin can call all apis, workflow_admin can only call ticket and workflow related apis and some common apis",
14 | "applicationList": "Application List",
15 | "searchWithKeyword": "Search With Keyword",
16 | "applicationDetial": "Application",
17 | "newApplication": "New Application",
18 | "tokenCopied": "Token Copied"
19 | },
20 | "notification": {
21 | "notificationList": "Notification",
22 | "notificationDetial": "Notification Detail",
23 | "newNotification": "New Notification",
24 | "notificationTypeDescription": "Notification Type Description"
25 | }
26 | }
--------------------------------------------------------------------------------
/sphinx_docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | # Use python -m sphinx to ensure we use the current Python environment
8 | SPHINXBUILD ?= python -m sphinx
9 | SOURCEDIR = source
10 | BUILDDIR = build
11 |
12 | # Put it first so that "make" without argument is like "make help".
13 | help:
14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
15 |
16 | .PHONY: help Makefile
17 |
18 | # Catch-all target: route all unknown targets to Sphinx using the new
19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
20 | %: Makefile
21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
22 |
23 | # gen po files
24 | update-translations:
25 | sphinx-intl update -p build/gettext -l zh_CN
26 |
27 | html-zh:
28 | sphinx-build -b html -D language=zh_CN source build/html/zh_CN
--------------------------------------------------------------------------------
/docker_compose_deploy/loonflow-backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.12.12-slim AS builder
2 | WORKDIR /app
3 |
4 |
5 | RUN apt-get update && apt-get install -y \
6 | build-essential \
7 | python3-dev \
8 | && rm -rf /var/lib/apt/lists/*
9 |
10 | COPY ./backend/requirements/common.txt .
11 | COPY ./backend/requirements/pro.txt .
12 |
13 | RUN pip install --user --no-warn-script-location -r pro.txt
14 |
15 |
16 | FROM python:3.12.12-slim
17 | LABEL maintainer=blackholll@163.com
18 |
19 | WORKDIR /app/loonflow
20 |
21 | ENV PYTHONUNBUFFERED=1 \
22 | PATH=/root/.local/bin:$PATH
23 |
24 |
25 | COPY --from=builder /root/.local /root/.local
26 |
27 | COPY ./backend /app/loonflow
28 |
29 | RUN cp settings/pro.py.sample settings/config.py && \
30 | sed -i "/HOMEPATH = os.environ/c\ HOMEPATH = '/var/log/loonflow'" /app/loonflow/settings/common.py
31 |
32 | RUN mkdir -p /var/log/loonflow
33 |
34 | # execution permisson
35 | # RUN chmod +x /app/loonflow/manage.py
36 |
37 | EXPOSE 8000
38 |
39 |
--------------------------------------------------------------------------------
/frontend/src/types/application.ts:
--------------------------------------------------------------------------------
1 | import { IApiResponse } from './common';
2 |
3 | // 定义 label 的类型为任意 key-value 对的字典对象
4 | type LabelDict = Record;
5 |
6 | export interface IApplicationResEntity {
7 | id: string;
8 | label: LabelDict; // 修改为字典对象类型
9 | createdAt: string;
10 | updatedAt: string;
11 | name: string;
12 | description: string;
13 | type: string;
14 | token: string;
15 | tenantId: string;
16 | }
17 |
18 | export interface ISimpleApplicationResEntity {
19 | id: string;
20 | name: string;
21 | description: string;
22 | type: string;
23 | tenantId?: string;
24 | }
25 | export interface IApplicationListResData {
26 | applicationInfoList: IApplicationResEntity[]
27 | }
28 |
29 | export interface ISimpleApplicationResData {
30 | applicationInfoList: ISimpleApplicationResEntity[]
31 | }
32 |
33 | export interface IApplicationListRes extends IApiResponse { }
34 | export interface ISimpleApplicationListRes extends IApiResponse { }
35 |
--------------------------------------------------------------------------------
/sphinx_docs/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version, and other tools you might need
8 | build:
9 | os: ubuntu-24.04
10 | tools:
11 | python: "3.12"
12 |
13 | # Build documentation with Sphinx
14 | # Note: This configuration file is in sphinx_docs/ directory
15 | sphinx:
16 | configuration: sphinx_docs/source/conf.py
17 | builder: html
18 |
19 | # Configure the languages supported
20 | # English is the default, Chinese (Simplified) is also supported
21 | formats:
22 | - htmlzip
23 | - pdf
24 |
25 | # Optionally, but recommended,
26 | # declare the Python requirements required to build your documentation
27 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
28 | python:
29 | install:
30 | - requirements: sphinx_docs/requirements.txt
31 |
32 | # Additional build options
33 | submodules:
34 | recursive: false
35 |
36 |
--------------------------------------------------------------------------------
/sphinx_docs/source/others/release_notes/r3.x/r3.0.1+.rst:
--------------------------------------------------------------------------------
1 | r3.0.1+
2 | ==================
3 |
4 | ---------
5 | r3.0.1
6 | ---------
7 | - Docker Compose: custom Redis/Postgres ports, drop version key, ARM build, pinned tags.
8 | - User-facing: reset personal password; choose workflow version on create; English publish hint; flow preview node text; default language follows browser.
9 | - Frontend: remove unused imports; upgrade yarn 4 / Node 22 / TS 5; incremental `yarn tsc`; type fixes; required label handled.
10 | - I18n: numbers/dates/components/templates covered; i18-ally config; default language follows browser.
11 | - Quality (workflow/forms): workflow version type editable; ticket field edits fixed; edge required check; user component drag fixed; rule editor i18n; notification editing fixed.
12 | - Quality (templates/dates): date/time placeholders fixed; title template parsing fixed (creator, select display, editing).
13 | - Ops: stdout logging restored; add initial user on deploy.
14 | - Docs: issue templates refreshed; user/API docs.
--------------------------------------------------------------------------------
/backend/service/manage/common_config_service.py:
--------------------------------------------------------------------------------
1 | from apps.account.models import Tenant, User
2 | from service.base_service import BaseService
3 |
4 |
5 | class CommonConfigService(BaseService):
6 | def __init__(self):
7 | pass
8 |
9 | @classmethod
10 | def get_common_config(cls, tenant_id, user_id):
11 | """
12 | get common config
13 | :param tenant_id:
14 | :param user_id:
15 | :return:
16 | """
17 | result = dict()
18 | tenant_record = Tenant.objects.get(id=tenant_id)
19 | result["tenant_id"] = tenant_record.id
20 | result["tenant_name"] = tenant_record.name
21 | result["tenant_icon"] = tenant_record.icon
22 | result["tenant_domain"] = tenant_record.domain
23 | result["lang"] = tenant_record.lang
24 | if user_id:
25 | user_record = User.objects.ge(id=user_id)
26 | result["lang"] = user_record.lang
27 | return result
28 |
29 |
30 | common_config_service_ins = CommonConfigService()
31 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/others/release_notes/index.rst:3
22 | msgid "Version History"
23 | msgstr "版本历史"
24 |
25 | #: ../../source/others/release_notes/index.rst:1
26 | msgid "This document contains release notes for all versions of Loonflow."
27 | msgstr "本文档包含 Loonflow 所有版本的发布说明。"
28 |
29 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/api_reference.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 18:56+0800\n"
11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 | "Last-Translator: FULL NAME \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/api_reference/postman.rst:2
22 | msgid "Postman Collection"
23 | msgstr "Postman 集合"
24 |
25 | #: ../../source/api_reference/postman.rst:5
26 | msgid "https://documenter.getpostman.com/view/15031929/2sB3WyJbap"
27 | msgstr "https://documenter.getpostman.com/view/15031929/2sB3WyJbap"
28 |
29 |
--------------------------------------------------------------------------------
/Roadmap_zh.md:
--------------------------------------------------------------------------------
1 | # 🗺️ 项目路线图
2 |
3 | ## 🎯 3.1.0 版本 (2026年2-3月)
4 | ### 核心功能增强
5 | - 📝 **表单字段扩展** - 新增文件上传、外部数据源、富文本编辑器字段类型
6 | - ⏰ **流程超时控制** - 支持节点超时自动流转,提升流程自动化程度
7 | - 🔍 **全文检索** - 实现工单和流程的全文搜索功能
8 | - 📋 **工作流分类** - 支持工作流分类管理,便于组织和管理
9 |
10 | ### 企业集成
11 | - 🔐 **OAuth认证** - 集成企业微信、飞书、钉钉、Azure等主流企业认证
12 | - 📢 **消息通知** - 支持企业微信、钉钉、飞书、Teams等平台的消息推送
13 | - 🔗 **子工单生成** - 支持配置规则自动生成子工单,实现复杂业务流程
14 |
15 | ### 权限与安全
16 | - 🛡️ **权限精细化** - 字段级权限控制,支持隐藏、脱敏、处理等操作
17 | - 👥 **创建权限控制** - 工作流创建权限的精细化配置
18 | - 📊 **Hook事件记录** - 完整的Hook事件查询和审计功能
19 |
20 | ### 开发与文档
21 | - 📚 **ReadTheDocs文档** - 完整的在线文档系统
22 | - 🧪 **单元测试** - 前后端完整的单元测试覆盖
23 | - 🌐 **后端国际化** - 后端API的国际化支持
24 | - 🐛 **Bug修复** - 持续的问题修复和小功能优化
25 |
26 | ## 中期规划 (2026年5月-6月)
27 | - 🚀 **性能优化** - 优化大数据量场景下的系统性能
28 | - 🔌 **插件生态** - 丰富官方插件库,支持更多业务场景
29 | - 📱 **移动端适配** - 优化移动设备上的使用体验
30 | - 🌐 **更多语言国际化** - 完善多国国际化** - 完整的多语言界面支持
31 |
32 | ## 长期愿景 (2026年下半年)
33 | - 🤖 **AI集成** - 集成AI能力,提供智能化的流程建议和自动化,智能分析工单数据,基于工单提供知识问答
34 | - 🔗 **生态集成** - 与更多主流企业系统深度集成
35 | - 📊 **数据分析** - 提供流程数据分析和优化建议
36 | - 🏢 **企业级功能** - 增强企业级部署和管理能力
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/api_reference/postman.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/api_reference/postman.rst:2
22 | msgid "Postman Collection"
23 | msgstr "Postman 集合"
24 |
25 | #: ../../source/api_reference/postman.rst:5
26 | msgid "https://documenter.getpostman.com/view/15031929/2sB3WyJbap"
27 | msgstr "https://documenter.getpostman.com/view/15031929/2sB3WyJbap"
28 |
29 |
--------------------------------------------------------------------------------
/frontend/craco.config.js:
--------------------------------------------------------------------------------
1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
2 |
3 | module.exports = {
4 | webpack: {
5 | configure: (webpackConfig) => {
6 | // 找到并移除 ForkTsCheckerWebpackPlugin
7 | webpackConfig.plugins = webpackConfig.plugins.filter(
8 | plugin => !(plugin instanceof ForkTsCheckerWebpackPlugin)
9 | );
10 |
11 | // 可选:如果你想保留类型检查但提高内存限制,可以这样配置:
12 | // webpackConfig.plugins = webpackConfig.plugins.map(plugin => {
13 | // if (plugin instanceof ForkTsCheckerWebpackPlugin) {
14 | // return new ForkTsCheckerWebpackPlugin({
15 | // ...plugin.options,
16 | // typescript: {
17 | // ...plugin.options.typescript,
18 | // memoryLimit: 8192, // 8GB 内存限制
19 | // },
20 | // });
21 | // }
22 | // return plugin;
23 | // });
24 |
25 | return webpackConfig;
26 | },
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/frontend/tsc-fast.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // 快速TypeScript编译脚本
4 | const { execSync } = require('child_process');
5 | const fs = require('fs');
6 | const path = require('path');
7 |
8 | console.log('🚀 开始快速TypeScript编译...');
9 |
10 | // 清理缓存
11 | try {
12 | if (fs.existsSync('tsconfig.tsbuildinfo')) {
13 | fs.unlinkSync('tsconfig.tsbuildinfo');
14 | console.log('✅ 清理了编译缓存');
15 | }
16 | } catch (error) {
17 | console.log('⚠️ 清理缓存失败:', error.message);
18 | }
19 |
20 | // 设置环境变量优化
21 | process.env.NODE_OPTIONS = '--max-old-space-size=8192';
22 |
23 | try {
24 | console.log('📦 开始编译...');
25 | const startTime = Date.now();
26 |
27 | // 使用优化的编译参数
28 | execSync('npx tsc --incremental --skipLibCheck --noEmit', {
29 | stdio: 'inherit',
30 | cwd: process.cwd()
31 | });
32 |
33 | const endTime = Date.now();
34 | const duration = (endTime - startTime) / 1000;
35 |
36 | console.log(`✅ 编译完成!耗时: ${duration.toFixed(2)}秒`);
37 |
38 | } catch (error) {
39 | console.error('❌ 编译失败:', error.message);
40 | process.exit(1);
41 | }
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDE and Editor Files
2 | .idea/
3 | .vscode/
4 | *.swp
5 |
6 | # Python
7 | *.pyc
8 | __pycache__/
9 | venv/
10 | .coverage
11 | coverage_html_report/
12 |
13 | # Node.js and Frontend
14 | node_modules/
15 | frontend/node_modules/
16 | /.pnp
17 | .pnp.js
18 | /coverage
19 | /build
20 | frontend/build/
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # Environment Files
26 | .env.local
27 | .env.development.local
28 | .env.test.local
29 | .env.production.local
30 |
31 | # Build and Documentation
32 | sphinx_docs/build/
33 | sphinx_docs/source/_build/
34 |
35 | # Media and User Uploads
36 | media/flowchart/*.*
37 | media/workflow_script/*.*
38 | media/ticket_file/*.*
39 | media/notice_script/*.*
40 | !media/workflow_script/demo_script.py
41 | !media/notice_script/demo_notice_script.py
42 |
43 | # Database and Configuration
44 | tests/testloonflow.sql
45 | settings/config.py
46 | loonflow_test.db
47 | docker_compose_deploy/loonflow_only/dbdata/*
48 | docker_compose_deploy/loonflow_shutongflow/dbdata/*
49 |
50 | # System Files
51 | .DS_Store
52 | clock_flag.txt
53 |
54 | *.mo
--------------------------------------------------------------------------------
/backend/service/hook/hook_base_service.py:
--------------------------------------------------------------------------------
1 | import fnmatch
2 | import requests
3 | from django.conf import settings
4 | from service.base_service import BaseService
5 | from service.common.common_service import common_service_ins
6 | from service.exception.custom_common_exception import CustomCommonException
7 |
8 |
9 | class HookBaseService(BaseService):
10 | @classmethod
11 | def call_hook(cls, hook_url: str, token: str, request_data_dict: dict) -> dict:
12 | hook_url_forbidden_list = settings.HOOK_HOST_FORBIDDEN
13 | for url in hook_url_forbidden_list:
14 | if fnmatch.fnmatch(hook_url, url):
15 | raise CustomCommonException("hook url is blocked, please contact system administrator to change HOOK_HOST_FORBIDDEN configure")
16 | signature, timestamp = common_service_ins.gen_signature_by_token(token)
17 | headers = dict(signature=signature, timestamp=timestamp)
18 | result = requests.post(hook_url, headers=headers, json=request_data_dict).json()
19 | return result
20 |
21 |
22 | hook_base_service_ins = HookBaseService()
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/organization/user_dept.rst:
--------------------------------------------------------------------------------
1 | User and Department Management
2 | ==============================
3 |
4 | Loonflow provides built-in user and department management. You can add, edit, and delete users or departments in the UI, and you can also synchronize them in bulk through the API.
5 |
6 | .. figure:: ../../images/user_dept_management.png
7 | :width: 100%
8 | :align: center
9 | :alt: user and department management
10 |
11 | How to manage
12 | -------------
13 | - UI maintenance: manually create or adjust users and departments on the “Organization Management” page—best for small-scale or ad-hoc changes.
14 | - API sync: call the organization management APIs to maintain users and departments in bulk. Populate the ``label`` field with an external system’s unique identifier (e.g., LDAP, AD) to simplify mapping and reconciliation later.
15 |
16 | Coming soon
17 | -----------
18 | Support for Azure AD, Google, DingTalk, and WeCom (WeChat Work) OAuth logins is on the way. After enabling, users and departments can be pulled from external identity sources without manual upkeep in the UI.
19 |
--------------------------------------------------------------------------------
/backend/tests/set_test_env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 从开发环境导出数据,并导入到测试环境
4 |
5 |
6 | echo " "
7 | echo " "
8 | echo "########### 开始从开发环境导出数据 ###########"
9 | /usr/local/mysql/bin/mysqldump -uloonflownew loonflownew > testloonflow.sql
10 | echo " "
11 | echo " "
12 | echo "########### 导出数据完成 ###########"
13 |
14 | echo " "
15 | echo " "
16 | echo "########### 开始导入数据到测试环境,输入测试环境数据库密码 ###########"
17 | /usr/local/mysql/bin/mysql -uloonflownew -h127.0.0.1 -p test_loonflownew < "testloonflow.sql"
18 | echo " "
19 | echo " "
20 |
21 | echo "########### 完成数据初始化 ###########"
22 |
23 | echo " "
24 | echo " "
25 |
26 | echo "你现在可以在上级目录执行python manage.py test tests/ --keepdb开始单元测试了"
27 | echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
28 | echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
29 |
--------------------------------------------------------------------------------
/frontend/src/components/Workflow/WorkflowDetail/checkWorkflowCompatibility.ts:
--------------------------------------------------------------------------------
1 | import { IWorkflowFullDefinition } from "../../../types/workflow";
2 |
3 | function checkWorkflowCompatibility(workflowDetailInfo: IWorkflowFullDefinition, workflowSourceDetailInfo: IWorkflowFullDefinition | null): Promise<{ isCompatible: boolean, messages: string[] }> {
4 | const messages: string[] = [];
5 | if (!workflowSourceDetailInfo) {
6 | return Promise.resolve({ isCompatible: true, messages });
7 | }
8 | // situaltions that not compatibility with old version: 1.node deleted
9 | const source_node = workflowSourceDetailInfo.processSchema.nodeInfoList;
10 | const detail_node = workflowDetailInfo.processSchema.nodeInfoList;
11 | for (const node of source_node) {
12 | if (!detail_node.find((detail_node) => detail_node.id === node.id)) {
13 | messages.push(`node(${node.name}) deleted`)
14 | return Promise.resolve({ isCompatible: false, messages });
15 | }
16 | }
17 | return Promise.resolve({ isCompatible: true, messages });
18 | }
19 |
20 | export default checkWorkflowCompatibility;
--------------------------------------------------------------------------------
/frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/edgeConditionValidation.ts:
--------------------------------------------------------------------------------
1 | import { IWorkflowFullDefinition } from '../../../../types/workflow';
2 | import { getValidationMessage } from './i18n';
3 |
4 | /**
5 | * Validate condition edges must have conditionGroups
6 | * @param workflowData Complete workflow definition data
7 | * @returns List of edge condition validation problems
8 | */
9 | export const validateEdgeConditions = (workflowData: IWorkflowFullDefinition): string[] => {
10 | const problems: string[] = [];
11 |
12 | // Check condition edges must have conditionGroups
13 | for (const edge of workflowData.processSchema.edgeInfoList) {
14 | if (edge.type === 'condition') {
15 | if (!edge.props.conditionGroups ||
16 | !Array.isArray(edge.props.conditionGroups) ||
17 | edge.props.conditionGroups.length === 0) {
18 | problems.push(getValidationMessage('edge', 'conditionGroupRequired', {
19 | edgeName: edge.name
20 | }));
21 | }
22 | }
23 | }
24 |
25 | return problems;
26 | };
27 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { Provider } from 'react-redux';
4 | import { PersistGate } from 'redux-persist/integration/react';
5 | import App from './App';
6 | import DynamicLocalizationProvider from './components/commonComponents/DynamicLocalizationProvider';
7 | import './i18n/index';
8 | import './index.css';
9 | import reportWebVitals from './reportWebVitals';
10 | import { persistor, store } from './store';
11 |
12 |
13 | const root = ReactDOM.createRoot(
14 | document.getElementById('root') as HTMLElement
15 | );
16 | root.render(
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 |
29 | // If you want to start measuring performance in your app, pass a function
30 | // to log results (for example: reportWebVitals(console.log))
31 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
32 | reportWebVitals();
33 |
--------------------------------------------------------------------------------
/docker_compose_deploy/loonflow-ui/nginx.conf:
--------------------------------------------------------------------------------
1 | user root;
2 | worker_processes auto;
3 | pid /run/nginx.pid;
4 |
5 | events {
6 | worker_connections 768;
7 | }
8 |
9 | http {
10 | sendfile on;
11 | tcp_nopush on;
12 | tcp_nodelay on;
13 | keepalive_timeout 65;
14 | types_hash_max_size 2048;
15 | client_max_body_size 1000M;
16 |
17 | include mime.types;
18 | default_type application/octet-stream;
19 |
20 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
21 | ssl_prefer_server_ciphers on;
22 |
23 | access_log /var/log/nginx/access.log;
24 | error_log /var/log/nginx/error.log;
25 |
26 | gzip on;
27 | gzip_disable "msie6";
28 |
29 | server {
30 | listen 80;
31 |
32 | access_log /var/log/loonflow/nginx_access.log;
33 | error_log /var/log/loonflow/nginx_error.log;
34 |
35 | root /usr/share/nginx/html;
36 | index index.html;
37 |
38 | location = / {
39 | try_files /index.html =404;
40 | }
41 |
42 | location / {
43 | try_files $uri $uri/ /index.html;
44 | }
45 |
46 | location /api {
47 | proxy_pass http://loonflow-backend:8000;
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/backend/apps/util/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2025-10-07 05:49
2 |
3 | from django.db import migrations, models
4 | import uuid
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Archive',
17 | fields=[
18 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
19 | ('label', models.JSONField(blank=True, default=dict, verbose_name='label')),
20 | ('creator_id', models.UUIDField(default=uuid.uuid4, editable=False, null=True, verbose_name='creator_id')),
21 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created_at')),
22 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated_at')),
23 | ('model_name', models.CharField(max_length=100, verbose_name='model name')),
24 | ('data', models.JSONField(verbose_name='archived data')),
25 | ],
26 | options={
27 | 'abstract': False,
28 | },
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/.github/workflows/image_build_backend.yml:
--------------------------------------------------------------------------------
1 | name: Build Backend Image
2 | on:
3 | push:
4 | tags:
5 | - 'r**'
6 | # branches:
7 | # - v3.0.1
8 | jobs:
9 | build_task:
10 | name: Build Task
11 | runs-on: ubuntu-latest
12 | steps:
13 | -
14 | name: Checkout
15 | uses: actions/checkout@v4
16 | -
17 | name: Set up QEMU
18 | uses: docker/setup-qemu-action@v3
19 | -
20 | name: Set up Docker Buildx
21 | uses: docker/setup-buildx-action@v3
22 | -
23 | name: Login to Docker Hub
24 | uses: docker/login-action@v3
25 | with:
26 | username: ${{ secrets.DOCKERHUB_USERNAME }}
27 | password: ${{ secrets.DOCKERHUB_TOKEN }}
28 | -
29 | name: Build and push
30 | uses: docker/build-push-action@v5
31 | with:
32 | context: .
33 | file: docker_compose_deploy/loonflow-backend/Dockerfile
34 | platforms: linux/amd64,linux/arm64
35 | cache-from: type=gha
36 | cache-to: type=gha,mode=max
37 | push: true
38 | # tags: blackholll/loonflow-backend:latest,blackholll/loonflow-backend:${{ github.ref_name }}
39 | tags: blackholll/loonflow-backend:${{ github.ref_name }}
40 |
--------------------------------------------------------------------------------
/frontend/src/utils/cookie.ts:
--------------------------------------------------------------------------------
1 | export const setCookie = (name: string, value: string, options: any = {}) => {
2 | options = {
3 | path: '/',
4 | ...options,
5 | };
6 |
7 | if (options.expires instanceof Date) {
8 | options.expires = options.expires.toUTCString();
9 | }
10 |
11 | let updatedCookie = `${name}=${value}`;
12 |
13 | for (let optionKey in options) {
14 | if (options.hasOwnProperty(optionKey)) {
15 | const optionValue = options[optionKey];
16 | if (optionValue !== undefined && optionValue !== '') {
17 | updatedCookie += `; ${optionKey}`;
18 | if (optionValue !== true) {
19 | updatedCookie += `=${optionValue}`;
20 | }
21 | }
22 | }
23 | }
24 |
25 | document.cookie = updatedCookie;
26 | };
27 |
28 | export const getCookie = (name: string): string | undefined => {
29 | const matches = document.cookie.match(new RegExp(`(?:^|; )${name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1')}=([^;]*)`));
30 | return matches ? decodeURIComponent(matches[1]) : undefined;
31 | };
32 |
33 | export const removeCookie = (name: string, options: any = {}) => {
34 | setCookie(name, '', {
35 | expires: -1,
36 | ...options,
37 | });
38 | };
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/zh-CN/ticket.json:
--------------------------------------------------------------------------------
1 | {
2 | "approveState": "审批状态",
3 | "keyword": "关键字",
4 | "creator": "创建人",
5 | "createDateStart": "创建起始日期",
6 | "createDateEnd": "创建结束日期",
7 | "ticketType": "工单类型",
8 | "ticketStatus": "工单状态",
9 | "ticketTitle": "工单标题",
10 | "ticketCreator": "工单创建人",
11 | "ticketCreateTime": "工单创建时间",
12 | "allTickets": "所有工单",
13 | "owerTickets": "我的工单",
14 | "viewTickets": "查看工单",
15 | "dutyTickets": "待办工单",
16 | "relationTickets": "关联工单",
17 | "interveneTickets": "干预工单",
18 | "adminActions": "管理员操作",
19 | "selectAssignee": "选择处理人",
20 | "operationRecord": "操作记录",
21 | "processor": "操作人",
22 | "actionType": "操作类型",
23 | "comment": "留言",
24 | "operationTime": "操作时间",
25 | "actionName": {
26 | "force_forward": "强制转发",
27 | "force_close": "强制关闭",
28 | "force_alter_node": "强制变更节点",
29 | "add_comment": "添加留言",
30 | "forward": "转发",
31 | "consult": "加签",
32 | "consult_submit": "加签完成",
33 | "accept": "接受",
34 | "withdraw": "撤回"
35 | },
36 | "state": {
37 | "in-draft": "草稿中",
38 | "on-going": "进行中",
39 | "rejected": "被拒绝",
40 | "withdrawn": "被撤回",
41 | "finished": "已完成",
42 | "closed": "已关闭"
43 | },
44 | "ticketCreated": "工单已创建",
45 | "ticketHandled": "工单已处理"
46 | }
47 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r0.x/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/others/release_notes/r0.x/index.rst:2
22 | msgid "r0.x Release Notes"
23 | msgstr "r0.x 发布说明"
24 |
25 | #: ../../source/others/release_notes/r0.x/index.rst:4
26 | msgid ""
27 | "For detailed release notes of v0.x versions, please refer to GitHub "
28 | "Releases:"
29 | msgstr "v0.x 版本的详细发布说明请参考 GitHub Releases:"
30 |
31 | #: ../../source/others/release_notes/r0.x/index.rst:6
32 | msgid "https://github.com/blackholll/loonflow/releases"
33 | msgstr "https://github.com/blackholll/loonflow/releases"
34 |
35 |
--------------------------------------------------------------------------------
/sphinx_docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. loonflow documentation master file, created by
2 | sphinx-quickstart on Sat Mar 7 12:17:00 2020.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to loonflow's documentation!
7 | ====================================
8 |
9 | .. toctree::
10 | :maxdepth: 3
11 | :caption: Introduction
12 |
13 | introduction/welcome/index
14 | introduction/who_need_read
15 |
16 | .. toctree::
17 | :maxdepth: 3
18 | :caption: Installation & Deployment
19 |
20 | installtion_deployment/system_requirements/index
21 | installtion_deployment/installation_guide/index
22 |
23 |
24 | .. toctree::
25 | :maxdepth: 3
26 | :caption: Core Concepts
27 |
28 | core_concepts/ticket_system_fundamentals/index
29 |
30 |
31 | .. toctree::
32 | :maxdepth: 3
33 | :caption: User Guide
34 |
35 | user_guide/ticket/index
36 | user_guide/workflow/index
37 | user_guide/organization/index
38 | user_guide/setting/index
39 |
40 | .. toctree::
41 | :maxdepth: 3
42 | :caption: API Reference
43 |
44 | api_reference/postman
45 |
46 | .. toctree::
47 | :maxdepth: 3
48 | :caption: Release Notes
49 |
50 | others/release_notes/index
51 |
52 |
53 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": [
6 | "./src/*"
7 | ],
8 | },
9 | "target": "ES2022",
10 | "lib": [
11 | "dom",
12 | "dom.iterable",
13 | "esnext"
14 | ],
15 | "allowJs": true,
16 | // "incremental": true,
17 | "skipLibCheck": true,
18 | "esModuleInterop": true,
19 | "allowSyntheticDefaultImports": true,
20 | "strict": false,
21 | "forceConsistentCasingInFileNames": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "module": "esnext",
24 | "moduleResolution": "node",
25 | "resolveJsonModule": true,
26 | "isolatedModules": true,
27 | "noEmit": true,
28 | "jsx": "react-jsx",
29 | "noUnusedLocals": false,
30 | "noUnusedParameters": false,
31 | "assumeChangesOnlyAffectDirectDependencies": true,
32 | "disableSourceOfProjectReferenceRedirect": true,
33 | "disableSolutionSearching": true,
34 | "disableReferencedProjectLoad": true,
35 | "noImplicitAny": false,
36 | "noImplicitReturns": false,
37 | "noImplicitThis": false,
38 | "allowImportingTsExtensions": false,
39 | "noEmit": true,
40 | "verbatimModuleSyntax": false
41 | },
42 | "include": [
43 | "src",
44 | "**/*.json"
45 | ],
46 | }
--------------------------------------------------------------------------------
/frontend/src/services/API.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace API {
2 | export interface CurrentUser {
3 | id: number;
4 | username: string;
5 | phone: string;
6 | isActive: boolean;
7 | typeId: number;
8 | email: string;
9 | }
10 |
11 | export interface CommonResponse {
12 | code: number;
13 | data: any;
14 | msg: string;
15 | }
16 |
17 | export interface LoginStateType {
18 | code?: number;
19 | data?: any;
20 | type?: string;
21 | }
22 |
23 | export interface NoticeIconData {
24 | id: string;
25 | key: string;
26 | avatar: string;
27 | title: string;
28 | datetime: string;
29 | type: string;
30 | read?: boolean;
31 | description: string;
32 | clickClose?: boolean;
33 | extra: any;
34 | status: string;
35 | }
36 |
37 | export interface TicketListData {
38 | code: number;
39 | data: any;
40 | msg: string;
41 | }
42 |
43 | export interface WorkflowListData {
44 | code: number;
45 | data: any;
46 | msg: string;
47 | }
48 |
49 | export interface WorkflowInitStateData {
50 | code: number;
51 | data: any;
52 | msg: string;
53 | }
54 |
55 | export interface queryUserSimpleData {
56 | code: number;
57 | data: any;
58 | msg: string;
59 | }
60 |
61 |
62 |
63 | }
64 |
65 |
66 |
--------------------------------------------------------------------------------
/frontend/src/store/authSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import camelcaseKeys from 'camelcase-keys';
3 | import { jwtDecode } from 'jwt-decode';
4 |
5 |
6 | interface AuthState {
7 | isAuthenticated: boolean;
8 | user: null | User
9 | }
10 |
11 | interface User {
12 | name: string,
13 | alias: string,
14 | email: string,
15 | type: string,
16 | tenantId: string,
17 | }
18 |
19 | const initialState: AuthState = {
20 | isAuthenticated: false,
21 | user: null
22 | };
23 |
24 | export const authSlice = createSlice({
25 | name: 'auth',
26 | initialState,
27 | reducers: {
28 | loginState: (state, action: PayloadAction) => {
29 | console.log('Before:', state);
30 | state.isAuthenticated = true;
31 | const decoded: any = jwtDecode(action.payload);
32 | state.user = camelcaseKeys(decoded.data, { deep: true }) as User;
33 | console.log('state111111:', state);
34 | console.log('statestatestatestatestatestate');
35 | console.log('After:', state); // 打印状态
36 | },
37 | logoutState: (state) => {
38 | state.isAuthenticated = false;
39 | state.user = null;
40 | },
41 | },
42 | });
43 |
44 | export const { loginState, logoutState } = authSlice.actions;
45 |
46 | export const authReducer = authSlice.reducer;
--------------------------------------------------------------------------------
/backend/tests/test_views/test_account_view.py:
--------------------------------------------------------------------------------
1 | import json
2 | from django.test import TestCase
3 | from django.test.client import Client
4 |
5 |
6 | class TestAccountView(TestCase):
7 | fixtures = ['accounts.json', 'workflows.json']
8 |
9 | def test_get_user_list_without_login(self):
10 | """
11 | get user list without login
12 | :return:
13 | """
14 | c = Client()
15 | response_content = c.get('/api/v1.0/accounts/users').content
16 | response_content_dict = json.loads(str(response_content, encoding='utf-8'))
17 | self.assertEqual(response_content_dict.get('code'), -1)
18 |
19 | def test_get_user_list_with_login(self):
20 | """
21 | get user list with login
22 | :return:
23 | """
24 | c = Client()
25 | # login
26 | login_content = c.post('/api/v1.0/login', json.dumps(dict(email='blackholll@163.com', password='123456')),content_type="application/json")
27 | login_response_dict = json.loads(login_content.content)
28 | jwt = login_response_dict.get("data").get("jwt")
29 | c.cookies.load(dict(jwt=jwt))
30 | response_content = c.get('/api/v1.0/accounts/users').content
31 | response_content_dict = json.loads(str(response_content, encoding='utf-8'))
32 | self.assertEqual(response_content_dict.get('code'), 0)
33 |
--------------------------------------------------------------------------------
/frontend/src/components/formFields/TextField.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, TextField as MuiTextField } from '@mui/material';
2 | import React from 'react';
3 | import ViewField from './ViewField';
4 |
5 | interface TextFieldProps {
6 | value: string;
7 | onChange: (value: string) => void;
8 | mode: 'view' | 'edit';
9 | props: any;
10 | }
11 |
12 | function TextField({
13 | value = '',
14 | onChange,
15 | mode,
16 | props,
17 | }: TextFieldProps) {
18 |
19 | const handleChange = (event: React.ChangeEvent) => {
20 | if (onChange) {
21 | onChange(event.target.value);
22 | }
23 | };
24 |
25 | // view mode only show value
26 | if (mode === 'view') {
27 | return (
28 |
29 | );
30 | }
31 |
32 | // edit mode, support edit
33 | return (
34 |
35 |
44 |
45 | );
46 |
47 | };
48 |
49 | export default TextField;
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/introduction/welcome/about_loonflow.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/introduction/welcome/about_loonflow.rst:2
22 | msgid "1.2 About Loonflow"
23 | msgstr "1.2 关于 Loonflow"
24 |
25 | #: ../../source/introduction/welcome/about_loonflow.rst:4
26 | msgid ""
27 | "Loonflow is an open-source process automation platform developed based on"
28 | " Django. After multiple versions of iteration and refactoring, we proudly"
29 | " present Loonflow 3.0 - a version that achieves a qualitative leap in "
30 | "visualization, flexibility, and scalability."
31 | msgstr "Loonflow 是基于 Django 开发的开源流程自动化平台。经过多次迭代与重构,我们自豪地推出 Loonflow 3.0——在可视化、灵活性和可扩展性上实现了质的飞跃。"
32 |
33 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/index.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 18:56+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/index.rst:9
22 | msgid "Introduction"
23 | msgstr "引言"
24 |
25 | #: ../../source/index.rst:16
26 | msgid "Installation & Deployment"
27 | msgstr "安装与部署"
28 |
29 | #: ../../source/index.rst:24
30 | msgid "Core Concepts"
31 | msgstr "核心概念"
32 |
33 | #: ../../source/index.rst:31
34 | msgid "User Guide"
35 | msgstr "用户指南"
36 |
37 | #: ../../source/index.rst:40
38 | msgid "API Reference"
39 | msgstr "API 参考"
40 |
41 | #: ../../source/index.rst:46
42 | msgid "Release Notes"
43 | msgstr "发行说明"
44 |
45 | #: ../../source/index.rst:7
46 | msgid "Welcome to loonflow's documentation!"
47 | msgstr "欢迎阅读 loonflow 文档!"
48 |
49 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/organization/role.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/user_guide/organization/role.rst:2
22 | msgid "Role Management"
23 | msgstr "角色管理"
24 |
25 | #: ../../source/user_guide/organization/role.rst:4
26 | msgid ""
27 | "Loonflow supports managing roles so you can create, edit, delete, and "
28 | "maintain role members. Roles are primarily used when configuring "
29 | "workflows to specify handlers, allowing you to assign a role as the "
30 | "processor for a workflow node."
31 | msgstr "Loonflow 支持角色管理,可创建、编辑、删除并维护角色成员。配置流程时可将角色指定为节点处理人。"
32 |
33 | #: ../../source/user_guide/organization/role.rst:6
34 | msgid "role management"
35 | msgstr "角色管理页面"
36 |
--------------------------------------------------------------------------------
/frontend/src/components/commonComponents/DynamicLocalizationProvider.tsx:
--------------------------------------------------------------------------------
1 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
2 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
3 | import dayjs from 'dayjs';
4 | import 'dayjs/locale/en';
5 | import 'dayjs/locale/zh-cn';
6 | import React from 'react';
7 | import { useTranslation } from 'react-i18next';
8 |
9 | interface DynamicLocalizationProviderProps {
10 | children: React.ReactNode;
11 | }
12 |
13 | function DynamicLocalizationProvider({ children }: DynamicLocalizationProviderProps) {
14 | const { i18n } = useTranslation();
15 |
16 | // 根据当前语言设置 dayjs locale
17 | const getDayjsLocale = React.useCallback(() => {
18 | switch (i18n.language) {
19 | case 'zh-CN':
20 | return 'zh-cn';
21 | case 'en-US':
22 | default:
23 | return 'en';
24 | }
25 | }, [i18n.language]);
26 |
27 | // 设置 dayjs locale
28 | React.useEffect(() => {
29 | const locale = getDayjsLocale();
30 | dayjs.locale(locale);
31 | }, [getDayjsLocale]);
32 |
33 | return (
34 |
38 | {children}
39 |
40 | );
41 | }
42 |
43 | export default DynamicLocalizationProvider;
44 |
--------------------------------------------------------------------------------
/backend/tests/fixtures/workflows.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "model": "workflow.workflow",
4 | "pk": 1,
5 | "fields": {
6 | "creator_id": 1,
7 | "created_at": "2018-04-23T20:49:32.229+08:00",
8 | "updated_at": "2018-10-22T08:05:15.574+08:00",
9 | "name": "\u8bf7\u5047\u7533\u8bf7",
10 | "label":"",
11 | "description": "\u8bf7\u5047\u7533\u8bf7",
12 | "notices": "2",
13 | "limit_expression": "{}",
14 | "display_form_str": "[\"sn\", \"title\", \"leave_start\", \"leave_end\", \"leave_days\", \"leave_proxy\", \"leave_type\", \"creator\", \"created_at\", \"leave_reason\"]",
15 | "title_template": "\u4f60\u6709\u4e00\u4e2a\u5f85\u529e\u5de5\u5355:{title}23",
16 | "content_template": "\u6807\u9898:{title}, \u521b\u5efa\u65f6\u95f4:{created_at}333"
17 | }
18 | },
19 | {
20 | "model": "workflow.workflow",
21 | "pk": 2,
22 | "fields": {
23 | "creator_id": 1,
24 | "created_at": "2018-05-06T12:32:36.690+08:00",
25 | "updated_at": "2018-11-05T23:32:57.667+08:00",
26 | "name": "vpn\u7533\u8bf7",
27 | "label":"",
28 | "description": "vpn\u6743\u9650\u7533\u8bf7",
29 | "notices": "",
30 | "limit_expression": "{}",
31 | "display_form_str": "[\"sn\", \"title\", \"model\", \"created_at\",\"participant.participant_alias\",\"vpn_reason\"]",
32 | "title_template": "",
33 | "content_template": ""
34 | }
35 | }
36 |
37 | ]
--------------------------------------------------------------------------------
/frontend/src/components/Ticket/NewTicketPage/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography } from '@mui/material';
2 | import { useEffect, useMemo } from 'react';
3 | import { useNavigate, useSearchParams } from 'react-router-dom';
4 | import useSnackbar from '../../../hooks/useSnackbar';
5 | import TicketDetail from '../TicketDetail';
6 |
7 | function NewTicketPage() {
8 | const [searchParams] = useSearchParams();
9 | const workflowId = useMemo(() => searchParams.get('workflow_id') || '', [searchParams]);
10 | const workflowVersionName = useMemo(() => searchParams.get('version_name') || undefined, [searchParams]);
11 | const { showMessage } = useSnackbar();
12 | const navigate = useNavigate();
13 |
14 | useEffect(() => {
15 | if (!workflowId) {
16 | showMessage('缺少 workflow_id 参数,无法创建工单', 'error');
17 | }
18 | }, [workflowId, showMessage]);
19 |
20 | const handleTicketCreated = (ticketId: string) => {
21 | showMessage('工单创建成功', 'success');
22 | navigate(`/ticket/${ticketId}`);
23 | };
24 |
25 | if (!workflowId) {
26 | return (
27 |
28 | 缺少 workflow_id 参数,无法创建工单。
29 |
30 | );
31 | }
32 |
33 | return (
34 |
39 | );
40 | }
41 |
42 | export default NewTicketPage;
43 |
--------------------------------------------------------------------------------
/frontend/src/components/formFields/TextAreaField.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TextField as MuiTextField, FormControl } from '@mui/material';
3 | import ViewField from './ViewField';
4 |
5 | interface TextAreaFieldProps {
6 | value: string;
7 | onChange: (value: string) => void;
8 | mode: 'view' | 'edit';
9 | props: any;
10 | }
11 |
12 | function TextAreaField({
13 | value = '',
14 | onChange,
15 | mode,
16 | props,
17 | }: TextAreaFieldProps) {
18 |
19 | const handleChange = (event: React.ChangeEvent) => {
20 | if (onChange) {
21 | onChange(event.target.value);
22 | }
23 | };
24 |
25 | // view mode only show value
26 | if (mode === 'view') {
27 | return (
28 |
29 | );
30 | }
31 |
32 | // edit mode, support edit
33 | return (
34 |
35 |
46 |
47 | );
48 |
49 | };
50 |
51 | export default TextAreaField;
--------------------------------------------------------------------------------
/frontend/src/services/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import camelcaseKeys from 'camelcase-keys';
3 | import snakecaseKeys from 'snakecase-keys';
4 | import { getCookie, removeCookie } from '../utils/cookie';
5 |
6 |
7 | const apiClient = axios.create({
8 | headers: {
9 | 'Content-Type': 'application/json',
10 | },
11 | });
12 |
13 | apiClient.interceptors.request.use(
14 | (config) => {
15 | const token = getCookie('jwtToken');
16 | if (token && !config.url?.includes('/api/v1.0/login')) {
17 | config.headers.Authorization = `Bearer ${token}`;
18 | }
19 | if (config.data && typeof config.data === 'object') {
20 | config.data = snakecaseKeys(config.data, { deep: true });
21 | }
22 | if (config.params && typeof config.params === 'object') {
23 | config.params = snakecaseKeys(config.params, { deep: true });
24 | }
25 | return config;
26 | },
27 | (error) => {
28 | return Promise.reject(error);
29 | }
30 | );
31 |
32 |
33 | apiClient.interceptors.response.use(
34 | (response) => {
35 | if (response.data && typeof response.data === 'object') {
36 | response.data = camelcaseKeys(response.data, { deep: true });
37 | }
38 | return response;
39 | },
40 | (error) => {
41 |
42 | if (error.response && error.response.status === 401) {
43 | removeCookie('jwtToken');
44 | window.location.href = '/signin';
45 | }
46 |
47 | return Promise.reject(error);
48 | }
49 | );
50 |
51 | export default apiClient;
--------------------------------------------------------------------------------
/frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/normalNodeValidation.ts:
--------------------------------------------------------------------------------
1 | import { IWorkflowFullDefinition } from '../../../../types/workflow';
2 | import { getValidationMessage } from './i18n';
3 |
4 | /**
5 | * Validate normal nodes
6 | * @param workflowData Complete workflow definition data
7 | * @returns List of normal node validation problems
8 | */
9 | export const validateNormalNodes = (workflowData: IWorkflowFullDefinition): string[] => {
10 | const problems: string[] = [];
11 |
12 | // Check normal node must has assignee
13 | for (const node of workflowData.processSchema.nodeInfoList) {
14 | if (node.type === 'normal') {
15 | // 检查处理人类型
16 | if (!node.props.assigneeType) {
17 | problems.push(getValidationMessage('normal', 'assigneeTypeRequired', {
18 | nodeName: node.name
19 | }));
20 | }
21 |
22 | // 检查用户/部门设置
23 | if (!node.props.assignee || node.props.assignee === '') {
24 | problems.push(getValidationMessage('normal', 'assigneeRequired', {
25 | nodeName: node.name
26 | }));
27 | }
28 |
29 | // 检查分配策略
30 | if (!node.props.assignmentStrategy) {
31 | problems.push(getValidationMessage('normal', 'assignmentStrategyRequired', {
32 | nodeName: node.name
33 | }));
34 | }
35 | }
36 | }
37 |
38 | return problems;
39 | };
40 |
--------------------------------------------------------------------------------
/backend/apps/manage/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2025-10-07 05:49
2 |
3 | from django.db import migrations, models
4 | import uuid
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Notification',
17 | fields=[
18 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
19 | ('label', models.JSONField(blank=True, default=dict, verbose_name='label')),
20 | ('creator_id', models.UUIDField(default=uuid.uuid4, editable=False, null=True, verbose_name='creator_id')),
21 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created_at')),
22 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated_at')),
23 | ('tenant_id', models.UUIDField(default='00000000-0000-0000-0000-000000000001', verbose_name='tenant_id')),
24 | ('name', models.CharField(default='', max_length=50, verbose_name='name')),
25 | ('description', models.CharField(default='', max_length=200, verbose_name='description')),
26 | ('type', models.CharField(default='', max_length=50, verbose_name='type')),
27 | ('extra', models.JSONField(max_length=1000, verbose_name='extra')),
28 | ],
29 | options={
30 | 'abstract': False,
31 | },
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/frontend/src/components/commonComponents/Snackbar/SnackbarProvider.tsx:
--------------------------------------------------------------------------------
1 | // src/components/Snackbar/SnackbarProvider.tsx
2 | import React, { createContext, ReactNode, useState } from 'react';
3 | import GlobalSnackbar from './GlobalSnackbar';
4 |
5 | interface SnackbarContextType {
6 | showMessage: (message: string | ReactNode, severity: 'error' | 'warning' | 'info' | 'success') => void;
7 | }
8 |
9 | export const SnackbarContext = createContext(undefined);
10 |
11 | interface SnackbarProviderProps {
12 | children: ReactNode;
13 | }
14 |
15 | const SnackbarProvider: React.FC = ({ children }) => {
16 | const [open, setOpen] = useState(false);
17 | const [message, setMessage] = useState('');
18 | const [severity, setSeverity] = useState<'error' | 'warning' | 'info' | 'success'>('info');
19 |
20 | const handleClose = (_event: React.SyntheticEvent | Event, reason?: string) => {
21 | if (reason === 'clickaway') {
22 | return;
23 | }
24 | setOpen(false);
25 | };
26 |
27 | const showMessage = (msg: string | ReactNode, sev: 'error' | 'warning' | 'info' | 'success') => {
28 | setMessage(msg);
29 | setSeverity(sev);
30 | setOpen(true);
31 | };
32 |
33 | return (
34 |
35 | {children}
36 |
42 |
43 | );
44 | };
45 |
46 | export default SnackbarProvider;
--------------------------------------------------------------------------------
/frontend/src/i18n/locales/en-US/ticket.json:
--------------------------------------------------------------------------------
1 | {
2 | "approveState": "Approve State",
3 | "keyword": "Search with keyword",
4 | "creator": "Creator",
5 | "createDateStart": "Create Date Start",
6 | "createDateEnd": "Create Date End",
7 | "ticketType": "Ticekt Type",
8 | "ticketStatus": "Ticket Status",
9 | "ticketTitle": "Ticket Title",
10 | "ticketCreator": "Ticket Creator",
11 | "ticketCreateTime": "Ticket Create Time",
12 | "allTickets": "All Tickets",
13 | "owerTickets": "Owner Tickets",
14 | "viewTickets": "View Tickets",
15 | "dutyTickets": "Duty Tickets",
16 | "relationTickets": "Relation Tickets",
17 | "interveneTickets": "Intervene Tickets",
18 | "adminActions": "Admin Actions",
19 | "selectAssignee": "Select Assignee",
20 | "operationRecord": "Operation Record",
21 | "processor": "processor",
22 | "actionType": "action type",
23 | "comment": "comment",
24 | "operationTime": "operation time",
25 | "actionName": {
26 | "force_forward": "force forward",
27 | "force_close": "force close",
28 | "force_alter_node": "force alter node",
29 | "add_comment": "add comment",
30 | "forward": "forward",
31 | "consult": "consult",
32 | "consult_submit": "consult submit",
33 | "accept": "accept",
34 | "withdraw": "withdraw"
35 | },
36 | "state": {
37 | "in-draft": "Draft",
38 | "on-going": "In Progress",
39 | "rejected": "Rejected",
40 | "withdrawn": "Withdrawn",
41 | "finished": "Finished",
42 | "closed": "Closed"
43 | },
44 | "ticketCreated": "Ticket Created",
45 | "ticketHandled": "ticket handled"
46 | }
47 |
--------------------------------------------------------------------------------
/backend/service/util/archive_service.py:
--------------------------------------------------------------------------------
1 | import json
2 | import time
3 |
4 | from django.forms.models import model_to_dict
5 |
6 | from service.base_service import BaseService
7 | from apps.util.models import Archive
8 | from django.core.serializers.json import DjangoJSONEncoder
9 |
10 |
11 | class ArchiveService(BaseService):
12 | """
13 | archive service
14 | """
15 | @classmethod
16 | def archive_record(cls, model_name, record, operator_id):
17 | """
18 | archive record
19 | :param model_name:
20 | :param record:
21 | :param operator_id:
22 | :return:
23 | """
24 | data = json.dumps(record.get_dict(), cls=DjangoJSONEncoder)
25 | archived_obj = Archive(data=data, model_name=model_name, creator_id=operator_id)
26 | archived_obj.save()
27 | record.delete()
28 | return True, ""
29 |
30 | @classmethod
31 | def archive_record_list(cls, model_name, record_queryset, operator_id):
32 | """
33 | archive record list
34 | :param model_name:
35 | :param record_queryset:
36 | :param operator_id:
37 | :return:
38 | """
39 | archive_list = []
40 | for record in record_queryset:
41 | data = json.dumps(record.get_dict(), cls=DjangoJSONEncoder)
42 | archive_list.append(Archive(data=data, model_name=model_name, creator_id=operator_id))
43 | Archive.objects.bulk_create(archive_list)
44 |
45 | record_queryset.delete()
46 | return True, ""
47 |
48 |
49 | archive_service_ins = ArchiveService()
50 |
--------------------------------------------------------------------------------
/backend/loonflow/urls.py:
--------------------------------------------------------------------------------
1 | """loonflow URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15 | """
16 | # from django.conf.urls import url, include
17 | from django.conf.urls.static import static
18 | from django.conf import settings
19 | from django.urls import path, include
20 | from apps.account.views import JwtLoginView
21 | from apps.manage.views import CommonConfigView
22 | from apps.workflow.views import WorkflowSimpleView
23 |
24 | urlpatterns = [
25 | path('', include('apps.manage.urls')),
26 | path('manage', include('apps.manage.urls')),
27 | path('api/v1.0/login', JwtLoginView.as_view()),
28 | path('api/v1.0/accounts', include('apps.account.urls')),
29 | path('api/v1.0/tickets', include('apps.ticket.urls')),
30 | path('api/v1.0/workflows', include('apps.workflow.urls')),
31 | path('api/v1.0/simple_workflows', WorkflowSimpleView.as_view()),
32 | path('api/v1.0/manage', include('apps.manage.urls')),
33 |
34 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
35 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/introduction/who_need_read.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/introduction/who_need_read.rst:2
22 | msgid "2. Who Need to Read This Guide"
23 | msgstr "2. 适用读者"
24 |
25 | #: ../../source/introduction/who_need_read.rst:4
26 | msgid "This guide is intended for:"
27 | msgstr "本指南适用于:"
28 |
29 | #: ../../source/introduction/who_need_read.rst:6
30 | msgid "**End Users** - Who submit and handle tickets in their daily work"
31 | msgstr "**终端用户** —— 日常提交和处理工单的用户"
32 |
33 | #: ../../source/introduction/who_need_read.rst:7
34 | msgid ""
35 | "**Admins** - Who are responsible for deploying, customizing, and "
36 | "maintaining the LoonFlow instance"
37 | msgstr "**管理员** —— 负责部署、定制和维护 LoonFlow 实例"
38 |
39 | #: ../../source/introduction/who_need_read.rst:8
40 | msgid ""
41 | "**Workflow Admins** - Who are responsible for configuring workflows, "
42 | "Intervene tickets"
43 | msgstr "**工作流管理员** —— 负责配置流程并可干预工单"
44 |
45 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
1 | name: '🪲 Report a bug'
2 | description: 'Report a bug that you have encountered'
3 | labels:
4 | - bug
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Please use English to describe your issue, otherwise the issue will be force closed. Search with keyword before reporting a new bug to avoid duplicates. Thank you!
10 | - type: textarea
11 | id: description
12 | validations:
13 | required: true
14 | attributes:
15 | label: '📝 Description & Context'
16 | placeholder: |
17 | Describe the observed behavior that you believe to be incorrect, as well as any additional context that might be useful in understanding the bug.
18 | - type: textarea
19 | id: expected-behavior
20 | validations:
21 | required: true
22 | attributes:
23 | label: '👍 Expected Behavior'
24 | placeholder: |
25 | Describe the behavior or result that you expected instead of the observed one. Please provide screenshots if needed
26 | - type: input
27 | id: version
28 | validations:
29 | required: true
30 | attributes:
31 | label: Version
32 | description: please use the released version, name is startswith 'r'. eg. r3.0.0, r.2.0.18 etc.
33 | - type: input
34 | id: os
35 | attributes:
36 | label: os
37 | description: if you think this is os related issue, please provide the os info.
38 | - type: input
39 | id: browser
40 | attributes:
41 | label: browser
42 | description: if you think this is browser related issue, please provide the browser and version info
43 |
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/ticket/handle_ticket.rst:
--------------------------------------------------------------------------------
1 | Handle Tickets
2 | ================
3 |
4 | After signing in to Loonflow, open your pending tickets from the left sidebar via **Workbench** or through **Ticket Management → My Duty**. Click **Detail** in the **Action** column to view a ticket. The detail page contains three main areas:
5 |
6 | .. figure:: ../../images/ticket_detail_page.png
7 | :width: 100%
8 | :align: center
9 | :alt: ticket detail page
10 |
11 |
12 | .. figure:: ../../images/workflow_diagram.png
13 | :width: 100%
14 | :align: center
15 | :alt: workflow diagram
16 |
17 | Ticket Detail
18 | --------------
19 | Shows all visible or editable fields for the ticket. Actions available to you appear under the fields and fall into four categories:
20 |
21 | - edge actions: Actions defined on the outgoing edges of the current workflow node.
22 | - viewer actions: Available to anyone who can view the ticket, e.g., `ADD COMMENT`.
23 | - handler actions: Available to users permitted to handle the ticket, e.g., `FORWARD`, `CONSULT`, `CONSULT_SUBMIT`.
24 | - special actions: Context-specific options such as `CONSULT_SUBMIT` (for assignees during a consultation) or `WITHDRAW` (when withdrawal is allowed on the current node and you are the ticket creator).
25 |
26 | Admin Actions
27 | --------------
28 | Visible when you are an administrator, the workflow manager for the ticket, or the ticket’s dispatcher. Options include `FORCE_FORWARD` and `FORCE_CLOSE`.
29 |
30 | Operation Record
31 | -----------------
32 | Displays the ticket’s activity history, including actor, time, action type, and details of each operation.
--------------------------------------------------------------------------------
/frontend/src/services/notification.ts:
--------------------------------------------------------------------------------
1 | import apiClient from './api';
2 |
3 |
4 | interface INotificationNew {
5 | name: string,
6 | description: string,
7 | type: string,
8 | extra: string
9 | }
10 |
11 | export const getNotificationList = async (searchValue: string, page: number, perPage: number) => {
12 | const response = await apiClient.get('/api/v1.0/manage/notifications', { params: { search_value: searchValue, page: page + 1, per_page: perPage } });
13 | return response.data;
14 | };
15 |
16 | export const getSimpleNotificationList = async (searchValue: string, page: number, perPage: number) => {
17 | const response = await apiClient.get('/api/v1.0/manage/simple_notifications', { params: { search_value: searchValue, page, per_page: perPage } });
18 | return response.data;
19 | };
20 |
21 | export const delNotification = async (notificationId: string) => {
22 | const response = await apiClient.delete(`/api/v1.0/manage/notifications/${notificationId}`);
23 | return response.data;
24 | };
25 | export const getNotificationDetail = async (notificationID: string) => {
26 | const response = await apiClient.get(`/api/v1.0/manage/notifications/${notificationID}`);
27 | return response.data
28 | };
29 |
30 | export const addNotification = async (params: INotificationNew) => {
31 | const response = await apiClient.post('/api/v1.0/manage/notifications', params);
32 | return response.data;
33 | }
34 |
35 | export const updateNotification = async (notification_id: string, params: INotificationNew) => {
36 | const response = await apiClient.patch(`/api/v1.0/manage/notifications/${notification_id}`, params);
37 | return response.data;
38 | }
--------------------------------------------------------------------------------
/sphinx_docs/source/installtion_deployment/system_requirements/os_environment_requirements.rst:
--------------------------------------------------------------------------------
1 | OS Environment Requirements
2 | ====================================
3 |
4 | This section describes the operating system requirements for running **LoonFlow**.
5 |
6 | Linux (Recommended)
7 | --------------------
8 |
9 | **LoonFlow is recommended to run on Linux operating systems, which is the preferred choice for production environments.**
10 |
11 | Supported All major Linux distributions:
12 |
13 | .. note::
14 | Linux operating system is strongly recommended for production environments to ensure system stability and performance.
15 |
16 | Windows (Development Only)
17 | --------------------------
18 |
19 | **Windows operating system is not recommended for running LoonFlow in production environments.**
20 |
21 | Since LoonFlow's asynchronous tasks are implemented using Celery, there may be compatibility issues on Windows systems, causing asynchronous tasks to fail to work properly. Here is some compatibility resoves https://stackoverflow.com/questions/37255548/how-to-run-celery-on-windows:
22 |
23 | .. warning::
24 | you can use Windows as a development environment, but may encounter issues such as asynchronous task execution exceptions.
25 |
26 | macOS (Development Only)
27 | ------------------------
28 |
29 | **macOS can be used as a development environment, but is not recommended for production environments.**
30 |
31 | Supported common macOS versions.
32 |
33 | .. note::
34 | macOS is suitable for local development and testing, but is not recommended for production environment deployment. Linux operating system should be used for production environments.
--------------------------------------------------------------------------------
/backend/tests/test_views/test_workflow_view.py:
--------------------------------------------------------------------------------
1 | import json
2 | from django.test import TestCase
3 | from tests.base import LoonflowApiCall
4 |
5 |
6 | class TestWorkflowView(TestCase):
7 | fixtures = ['accounts.json', 'workflows.json']
8 |
9 | def test_get_workflow_list_without_arg(self):
10 | """
11 | 获取工作流列表
12 | :return:
13 | """
14 | url = '/api/v1.0/workflows'
15 | response_content_dict = LoonflowApiCall().api_call('get', url)
16 | self.assertEqual(response_content_dict.get('code'), 0)
17 | self.assertGreater(len(response_content_dict.get('data')), 1)
18 |
19 | def test_get_workflow_init_state(self):
20 | """
21 | 获取工作流初始状态
22 | :return:
23 | """
24 | workflow_id = 1
25 | url = '/api/v1.0/workflows/{}/init_state'.format(workflow_id)
26 | response_content_dict = LoonflowApiCall().api_call('get', url)
27 | self.assertEqual(response_content_dict.get('code'), 0)
28 |
29 | def test_get_workflow_states(self):
30 | """
31 | 获取工作流状态列表
32 | :return:
33 | """
34 | workflow_id = 1
35 | url = '/api/v1.0/workflows/{}/states'.format(workflow_id)
36 | response_content_dict = LoonflowApiCall().api_call('get', url)
37 | self.assertEqual(response_content_dict.get('code'), 0)
38 |
39 | def test_get_workflow_state_detail(self):
40 | """
41 | 获取状态详情
42 | :return:
43 | """
44 | state_id = 3
45 | url = '/api/v1.0/workflows/states/{}'.format(state_id)
46 | response_content_dict = LoonflowApiCall().api_call('get', url)
47 | self.assertEqual(response_content_dict.get('code'), 0)
48 |
--------------------------------------------------------------------------------
/backend/settings/test.py:
--------------------------------------------------------------------------------
1 | from settings.common import *
2 |
3 | # for multi computer room deploy and use separate redis server
4 | DEPLOY_ZONE = ''
5 |
6 | MIDDLEWARE = [
7 | 'service.csrf_service.DisableCSRF',
8 | 'django.middleware.security.SecurityMiddleware',
9 | 'django.contrib.sessions.middleware.SessionMiddleware',
10 | 'django.middleware.common.CommonMiddleware',
11 | 'django.middleware.csrf.CsrfViewMiddleware',
12 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
13 | 'service.permission.app_permission.AppPermissionCheck',
14 | 'django.contrib.messages.middleware.MessageMiddleware',
15 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
16 | ]
17 |
18 |
19 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
20 | MEDIA_URL = '/media/'
21 |
22 | # Database
23 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
24 |
25 | DATABASES = {
26 | 'default': {
27 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
28 | 'NAME': 'loonflow_test.db', # Or path to database file if using sqlite3.
29 | 'TEST_CHARSET': 'utf8',
30 | 'TEST_COLLATION': 'utf8_general_ci',
31 | }
32 | }
33 |
34 | REDIS_HOST = '127.0.0.1'
35 | REDIS_PORT = 6379
36 | REDIS_DB = 0
37 | REDIS_PASSWORD = ''
38 |
39 | if REDIS_PASSWORD:
40 | CELERY_BROKER_URL = 'redis://:{}@{}:{}/{}'.format(REDIS_PASSWORD, REDIS_HOST, REDIS_PORT, REDIS_DB)
41 | else:
42 | CELERY_BROKER_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_DB)
43 |
44 | FIXTURE_DIRS = ('tests/fixtures/',)
45 |
46 | ENCRYPTION_KEY = '2VLMQOroSgJUC68n30X9VzFkUPzN0oYpprGlwy/ffmk=' # you can generate your key, refer to document
--------------------------------------------------------------------------------
/frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/formValidation.ts:
--------------------------------------------------------------------------------
1 | import { IWorkflowFullDefinition } from '../../../../types/workflow';
2 | import { getValidationMessage } from './i18n';
3 |
4 | /**
5 | * Validate form schema
6 | * @param workflowData Complete workflow definition data
7 | * @returns List of form validation problems
8 | */
9 | export const validateFormSchema = (workflowData: IWorkflowFullDefinition): string[] => {
10 | const problems: string[] = [];
11 |
12 | // Check if form design is empty
13 | if (workflowData.formSchema.componentInfoList.length === 0) {
14 | problems.push(getValidationMessage('form', 'formDesignEmpty'));
15 | }
16 |
17 | // Check if every row component has children
18 | for (const component of workflowData.formSchema.componentInfoList) {
19 | if (component.type === 'row') {
20 | if (component.children.length === 0) {
21 | problems.push(getValidationMessage('form', 'rowComponentNoChildren'));
22 | }
23 | }
24 | }
25 |
26 | // Rule: title component MUST exist exactly once
27 | // Flatten components to count title components
28 | const allComponents = workflowData.formSchema.componentInfoList.flatMap((component: any) => {
29 | if (component.type === 'row') {
30 | return component.children || [];
31 | }
32 | return [component];
33 | });
34 |
35 | const titleComponents = allComponents.filter((c: any) => c.type === 'title');
36 | if (titleComponents.length !== 1) {
37 | problems.push(getValidationMessage('form', 'titleComponentCountError', {
38 | count: titleComponents.length
39 | }));
40 | }
41 |
42 | return problems;
43 | };
44 |
--------------------------------------------------------------------------------
/backend/service/util/encrypt_service.py:
--------------------------------------------------------------------------------
1 | import os
2 | import base64
3 | from django.conf import settings
4 | from service.base_service import BaseService
5 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
6 | from cryptography.hazmat.backends import default_backend
7 | from cryptography.hazmat.primitives import padding
8 |
9 |
10 | class EncryptService(BaseService):
11 | @classmethod
12 | def encrypt(cls, data:str)->str:
13 | """
14 | AES encrypt
15 | """
16 | encrypt_key = settings.ENCRYPTION_KEY
17 | iv = os.urandom(16)
18 | padder = padding.PKCS7(128).padder()
19 | padded_data = padder.update(data.encode()) + padder.finalize()
20 | cipher = Cipher(algorithms.AES(base64.b64decode(encrypt_key)), modes.CBC(iv), backend=default_backend())
21 | encryptor = cipher.encryptor()
22 | encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
23 | return base64.b64encode(iv).decode('utf-8') +',' + base64.b64encode(encrypted_data).decode('utf-8')
24 |
25 | @classmethod
26 | def decrypt(cls, data:str)->str:
27 | encrypt_key = settings.ENCRYPTION_KEY
28 | iv = base64.b64decode(data.split(',')[0])
29 | encrypted_data = base64.b64decode(data.split(',')[1])
30 | cipher = Cipher(algorithms.AES(base64.b64decode(encrypt_key)), modes.CBC(iv), backend=default_backend())
31 | decryptor = cipher.decryptor()
32 | decrypted_padded_data = decryptor.update(encrypted_data) + decryptor.finalize()
33 | unpadder = padding.PKCS7(128).unpadder()
34 | data = unpadder.update(decrypted_padded_data) + unpadder.finalize()
35 | return data.decode()
36 |
37 |
38 | encrypt_service_ins = EncryptService()
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue Report
3 | about: Report LoonFlow issues, feature requests, or questions
4 | title: ''
5 | labels: ['bug', 'question', 'enhancement']
6 | assignees: ''
7 | ---
8 |
9 | ## ⚠️ IMPORTANT: Language Requirement
10 | **This issue MUST be written in English. Issues written in other languages will be closed immediately without review.**
11 |
12 | ## Issue Type
13 |
14 | - [ ] Bug Report
15 | - [ ] Feature Request
16 | - [ ] Question/Help
17 | - [ ] Other
18 |
19 | ## LoonFlow Version
20 |
21 |
22 | ## Description
23 |
24 |
25 | ## System Logs Check
26 |
27 | - [ ] Checked system logs
28 | - [ ] Not checked system logs
29 | - [ ] Not applicable
30 |
31 | ## Historical Issues Search
32 |
33 | - [ ] Searched historical issues
34 | - [ ] Not searched historical issues
35 |
36 | ## Attempted Solutions
37 |
38 |
39 | ## Environment Information
40 | - Operating System:
41 | - Python Version:
42 | - Django Version:
43 | - Database Type:
44 | - Browser Type and Version:
45 |
46 | ## Steps to Reproduce (Bug Reports Only)
47 | 1.
48 | 2.
49 | 3.
50 |
51 | ## Expected Behavior
52 |
53 |
54 | ## Actual Behavior
55 |
56 |
57 | ## Additional Information
58 |
--------------------------------------------------------------------------------
/sphinx_docs/source/others/release_notes/r1.x/r1.0.0.rst:
--------------------------------------------------------------------------------
1 | r1.0.0
2 | ---------
3 |
4 | - Upgraded to Python 3.6
5 | - Unified configuration file to config.py
6 | - New API: Withdraw ticket
7 | - Ticket details API added return of current state detailed information
8 | - Allow ticket creator to close ticket directly in initial state
9 | - Ticket list API performance optimization
10 | - flowstep API added return of current state information, and records sorted by state order id
11 | - Ticket list query API added support for query conditions: draft, in progress, withdrawn, returned, completed
12 | - Custom notifications changed from script to hook method
13 | - Admin panel homepage added ticket quantity classification statistics
14 | - Admin panel displays current detailed version number
15 | - Admin panel supports user, department, and role editing
16 | - Admin panel hides handler input box information for initial and end states when configuring states
17 | - Admin panel supports ticket intervention: directly close, transfer, modify ticket state, delete
18 | - Support setting multiple departments when state participant type is department
19 | - Transition operation supports target state as initial state: no longer need to configure an intermediate state like "Creator Editing"
20 | - Workflow state hook supports configuring additional parameter information
21 | - Admin panel permission control refined: divided into super administrator and workflow administrator
22 | - Using readthedoc to manage project documentation
23 | - Static files moved from CDN to local to avoid inability to use normally when internal network deployment has no external network access
24 | - Code structure and internal logic optimization (removed redundant code, singleton pattern to reduce memory usage, database operation statement optimization, type hints, view parameter strong validation, etc.)
25 |
--------------------------------------------------------------------------------
/sphinx_docs/source/user_guide/ticket/query_ticket.rst:
--------------------------------------------------------------------------------
1 | Query tickets
2 | =============
3 |
4 | After signing in to Loonflow you can browse different ticket lists. The workbench shows your pending tasks. Under the “Ticket management” menu you will find:
5 |
6 | - `My Duty`: tickets currently assigned to you
7 | - `My Owner`: tickets you created
8 | - `My Relation`: tickets you are related to (created by you, previously assigned to you, handled by you, commented by you, or force‑intervened by you)
9 | - `My View`: tickets in workflows where you have view permission
10 | - `My Intervene`: tickets whose workflows list you as Dispatcher
11 | - `All Tickets`: every ticket; only administrators can view and intervene through this entry
12 |
13 | .. figure:: ../../images/category_tickets.png
14 | :width: 100%
15 | :align: center
16 | :alt: Category Tickets
17 |
18 | Each ticket list provides filters above the table: `Keyword`, `Creator`, `Created time range`, and `Workflow` (ticket type). `Keyword` currently supports fuzzy search on the ticket title only. To enable searching by custom fields, set the title field to `Auto Generate` in the workflow form designer and include the desired fields in the title template. The system will then auto-generate titles containing those field values, letting you find tickets via those custom fields.
19 |
20 | .. figure:: ../../images/title_property.png
21 | :width: 100%
22 | :align: center
23 | :alt: title property
24 |
25 | .. note::
26 | if you set title property to "Auto Generate", the system will auto-generate a title for the ticket based on the custom fields. so you must set the title field's permission to "hidden" for the start node.
27 |
28 |
29 | .. figure:: ../../images/start_node_title_auto_generate.png
30 | :width: 100%
31 | :align: center
32 | :alt: start node properly if title field is auto_generate
--------------------------------------------------------------------------------
/backend/apps/loon_base_view.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import simplejson
3 | from django.views import View
4 | from schema import SchemaError
5 |
6 | from service.format_response import api_response
7 |
8 | logger = logging.getLogger('django')
9 |
10 |
11 | class BaseView(View):
12 | """
13 | base view for params validate
14 | """
15 | def dispatch(self, request, *args, **kwargs):
16 | # Try to dispatch to the right method; if a method doesn't exist,
17 | # defer to the error handler. Also defer to the error handler if the
18 | # request method isn't on the approved list.
19 | if request.method.lower() in self.http_method_names:
20 | handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
21 | else:
22 | handler = self.http_method_not_allowed
23 | request_method = request.method.lower()
24 | meth_schema = getattr(self, request.method.lower() + '_schema', None)
25 | if meth_schema:
26 | if request_method in ['post', 'patch', 'put', 'delete']:
27 | json_dict = simplejson.loads(request.body)
28 | else:
29 | request_data_dict = dict(request.GET)
30 | json_dict = dict()
31 | for key, value in request_data_dict.items():
32 | json_dict[key] = ','.join(value)
33 |
34 | try:
35 | meth_schema.validate(json_dict)
36 | except SchemaError as Se:
37 | logger.error('111111')
38 | logger.error(Se)
39 | return api_response(-1, 'Request data is invalid:{}'.format(str(Se)), {})
40 | except Exception as e:
41 | logger.error('2222')
42 | logger.error(e)
43 | return api_response(-1, 'Internal Server Error', {})
44 | return handler(request, *args, **kwargs)
45 |
--------------------------------------------------------------------------------
/backend/settings/test.py.sample:
--------------------------------------------------------------------------------
1 | from settings.common import *
2 |
3 | # for multi computer room deploy and use separate redis server
4 | DEPLOY_ZONE = ''
5 |
6 | MIDDLEWARE = [
7 | 'service.csrf_service.DisableCSRF',
8 | 'django.middleware.security.SecurityMiddleware',
9 | 'django.contrib.sessions.middleware.SessionMiddleware',
10 | 'django.middleware.common.CommonMiddleware',
11 | 'django.middleware.csrf.CsrfViewMiddleware',
12 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
13 | 'service.permission.app_permission.AppPermissionCheck',
14 | 'django.contrib.messages.middleware.MessageMiddleware',
15 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
16 | ]
17 |
18 |
19 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
20 | MEDIA_URL = '/media/'
21 |
22 | # Database
23 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
24 |
25 | DATABASES = {
26 | 'default': {
27 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
28 | 'NAME': 'loonflow_test.db', # Or path to database file if using sqlite3.
29 | 'TEST_CHARSET': 'utf8',
30 | 'TEST_COLLATION': 'utf8_general_ci',
31 | }
32 | }
33 |
34 | REDIS_HOST = '127.0.0.1'
35 | REDIS_PORT = 6379
36 | REDIS_DB = 0
37 | REDIS_PASSWORD = ''
38 |
39 | if REDIS_PASSWORD:
40 | CELERY_BROKER_URL = 'redis://:{}@{}:{}/{}'.format(REDIS_PASSWORD, REDIS_HOST, REDIS_PORT, REDIS_DB)
41 | else:
42 | CELERY_BROKER_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_DB)
43 |
44 | FIXTURE_DIRS = ('tests/fixtures/',)
45 |
46 | HOOK_HOST_ALLOWED = [] # host list that allow used as hook url, such as ['192,168.1.12', 'www.baidu.com'], if no this setting key means allow
47 |
48 | JWT_SALT = 'aUApFqfQjyYVAPo8'
49 | ENCRYPTION_KEY = '2VLMQOroSgJUC68n30X9VzFkUPzN0oYpprGlwy/ffmk=' # you can generate your key, refer to document
--------------------------------------------------------------------------------
/backend/service/account/account_tenant_service.py:
--------------------------------------------------------------------------------
1 | from apps.account.models import Tenant
2 | from service.base_service import BaseService
3 | from service.exception.custom_common_exception import CustomCommonException
4 |
5 |
6 | class AccountTenantService(BaseService):
7 |
8 | @classmethod
9 | def get_tenant_detail(cls, tenant_id):
10 | """
11 | get tenant detail info
12 | :param tenant_id:
13 | :return:
14 | """
15 | try:
16 | tenant_obj = Tenant.objects.get(id=tenant_id)
17 | except Tenant.DoesNotExist as e:
18 | raise CustomCommonException("tenant is not exist or has been deleted")
19 | except:
20 | raise
21 | result = tenant_obj.get_dict()
22 | result.pop("creator_info")
23 | return result
24 |
25 | @classmethod
26 | def get_tenant_by_domain(cls, domain):
27 | """
28 | get tenant detail info by domain
29 | :param domain: domain name
30 | :return: tenant info dict
31 | """
32 | try:
33 | tenant_obj = Tenant.objects.get(domain=domain)
34 | except Tenant.DoesNotExist:
35 | # If tenant not found by domain, return default tenant with id '00000000-0000-0000-0000-000000000001'
36 | try:
37 | tenant_obj = Tenant.objects.get(id='00000000-0000-0000-0000-000000000001')
38 | except Tenant.DoesNotExist:
39 | raise CustomCommonException("Default tenant not found")
40 | except Exception:
41 | raise
42 | except Exception:
43 | raise
44 | result = tenant_obj.get_dict()
45 | res_result = {
46 | 'id': result['id'],
47 | 'name': result['name'],
48 | 'domain': result['domain'],
49 | 'logo_path': result['logo_path'],
50 | }
51 | return res_result
52 |
53 |
54 | account_tenant_service_ins = AccountTenantService()
55 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/setting/application.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/user_guide/setting/application.rst:2
22 | msgid "Application Management"
23 | msgstr "应用管理"
24 |
25 | #: ../../source/user_guide/setting/application.rst:4
26 | msgid ""
27 | "Applications are used to authenticate API calls. After creating an "
28 | "application, a token is generated and should be used to sign each "
29 | "request. The signing algorithm is documented in the Postman collection: "
30 | "https://documenter.getpostman.com/view/15031929/2sB3WyJbap"
31 | msgstr "应用用于鉴权 API 调用。创建应用后会生成 token,需在每次请求中签名,签名算法见 Postman 集合:https://documenter.getpostman.com/view/15031929/2sB3WyJbap"
32 |
33 | #: ../../source/user_guide/setting/application.rst:6
34 | msgid "Application management page"
35 | msgstr "应用管理页面"
36 |
37 | #: ../../source/user_guide/setting/application.rst:13
38 | msgid "Application types"
39 | msgstr "应用类型"
40 |
41 | #: ../../source/user_guide/setting/application.rst:14
42 | msgid "admin: Administrators can call all APIs."
43 | msgstr "admin:管理员可调用全部接口"
44 |
45 | #: ../../source/user_guide/setting/application.rst:15
46 | msgid ""
47 | "workflow_admin: Workflow administrators can call workflow/ticket APIs and"
48 | " common APIs."
49 | msgstr "workflow_admin:流程管理员可调用工作流/工单及公共接口"
50 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
27 | Loonflow
28 |
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/introduction/welcome/getting_help.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/introduction/welcome/getting_help.rst:2
22 | msgid "1.3 Get Help & Community Support"
23 | msgstr "1.3 获取帮助与社区支持"
24 |
25 | #: ../../source/introduction/welcome/getting_help.rst:4
26 | msgid ""
27 | "📝 GitHub Issues - Submit bug reports and feature requests. "
28 | "https://github.com/blackholll/loonflow/issues"
29 | msgstr "📝 GitHub Issues - 提交缺陷与功能需求:https://github.com/blackholll/loonflow/issues"
30 |
31 | #: ../../source/introduction/welcome/getting_help.rst:5
32 | msgid "💬 Discussion Forum Discord. https://discord.gg/WuppaG638k"
33 | msgstr "💬 讨论论坛 Discord:https://discord.gg/WuppaG638k"
34 |
35 | #: ../../source/introduction/welcome/getting_help.rst:6
36 | msgid ""
37 | "📧 Commercial Support & Customization: For enterprise-level deep "
38 | "customization, technical training, or deployment support needs, please "
39 | "contact me at [blackholll@163.com;blackholll.cn@gmail.com]."
40 | msgstr "📧 商业支持与定制:如需企业级深度定制、技术培训或部署支持,请联系 [blackholll@163.com;blackholll.cn@gmail.com]。"
41 |
42 | #: ../../source/introduction/welcome/getting_help.rst:7
43 | msgid "💰 Member Benefits. See https://patreon.com/blackholllcn for more details."
44 | msgstr "💰 会员权益:详见 https://github.com/blackholll/loonflow/blob/master/README_zh.md"
45 |
46 |
--------------------------------------------------------------------------------
/frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/timerNodeValidation.ts:
--------------------------------------------------------------------------------
1 | import { IWorkflowFullDefinition } from '../../../../types/workflow';
2 | import { getValidationMessage } from './i18n';
3 |
4 | /**
5 | * Validate timer nodes
6 | * @param workflowData Complete workflow definition data
7 | * @returns List of timer node validation problems
8 | */
9 | export const validateTimerNodes = (workflowData: IWorkflowFullDefinition): string[] => {
10 | const problems: string[] = [];
11 |
12 | for (const node of workflowData.processSchema.nodeInfoList) {
13 | if (node.type === 'timer') {
14 | // 获取定时器节点的入边和出边
15 | const inputEdges = workflowData.processSchema.edgeInfoList.filter(
16 | (edge) => edge.targetNodeId === node.id
17 | );
18 | const outputEdges = workflowData.processSchema.edgeInfoList.filter(
19 | (edge) => edge.sourceNodeId === node.id
20 | );
21 |
22 | // 检查定时器节点必须有超过一个入边
23 | if (inputEdges.length < 1) {
24 | problems.push(getValidationMessage('timer', 'needInputEdge', {
25 | nodeName: node.name,
26 | count: inputEdges.length
27 | }));
28 | }
29 |
30 | // 检查定时器节点只能有一个出边
31 | if (outputEdges.length !== 1) {
32 | problems.push(getValidationMessage('timer', 'onlyOneOutputEdge', {
33 | nodeName: node.name,
34 | count: outputEdges.length
35 | }));
36 | }
37 |
38 | // 检查定时器节点的出边目标节点不可以是自己
39 | for (const outputEdge of outputEdges) {
40 | if (outputEdge.targetNodeId === node.id) {
41 | problems.push(getValidationMessage('timer', 'outputEdgeCannotBeSelf', {
42 | nodeName: node.name
43 | }));
44 | }
45 | }
46 | }
47 | }
48 |
49 | return problems;
50 | };
51 |
--------------------------------------------------------------------------------
/backend/apps/account/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from apps.account.views import UserView, UserDetailView, RoleView, DeptView, UserRoleView, RoleUserView, \
3 | UserResetPasswordView, RoleDetailView, \
4 | DeptDetailView, SimpleUsersView, UserChangePasswordView, DeptPathsView,DeptPathView,\
5 | DeptTreeView, UserProfileView, SimpleRolesView, ApplicationView, SimpleApplicationView, \
6 | ApplicationDetailView, TenantDetailView, TenantDomainView, DeptParentDeptView, ApplicationWorkflowPermissionListView
7 |
8 | urlpatterns = [
9 | path('/users', UserView.as_view()),
10 | path('/my_profile', UserProfileView.as_view()),
11 | path('/simple_users', SimpleUsersView.as_view()),
12 | path('/users/change_password', UserChangePasswordView.as_view()),
13 | path('/users/', UserDetailView.as_view()),
14 | path('/users//roles', UserRoleView.as_view()),
15 | path('/users//reset_password', UserResetPasswordView.as_view()),
16 | path('/roles', RoleView.as_view()),
17 | path('/roles/', RoleDetailView.as_view()),
18 | path('/simple_roles', SimpleRolesView.as_view()),
19 | path('/roles//users', RoleUserView.as_view()),
20 | path('/depts_tree', DeptTreeView.as_view()),
21 | path('/dept_paths', DeptPathsView.as_view()),
22 | path('/dept_paths/', DeptPathView.as_view()),
23 | path('/depts', DeptView.as_view()),
24 | path('/depts/', DeptDetailView.as_view()),
25 | path('/depts//parent_dept', DeptParentDeptView.as_view()),
26 | path('/applications', ApplicationView.as_view()),
27 | path('/applications/', ApplicationDetailView.as_view()),
28 | path('/applications//workflows', ApplicationWorkflowPermissionListView.as_view()),
29 | path('/simple_applications', SimpleApplicationView.as_view()),
30 | path('/tenants/by_domain', TenantDomainView.as_view()),
31 | path('/tenants/', TenantDetailView.as_view())
32 | ]
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yaml:
--------------------------------------------------------------------------------
1 | name: '💡 Request a feature'
2 | description: 'Request a new feature or change to loonflow'
3 | labels:
4 | - enhancement
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | This template helps your propose new feature, enhancements, or other ideas for Loonflow. Please use English to describe your request, otherwise the issue will be force closed.
10 | - type: textarea
11 | id: need
12 | validations:
13 | required: true
14 | attributes:
15 | label: '🔖 Need'
16 | description: What is the rationale for this change, and who will benefit from it?
17 | placeholder: |
18 | Explain why this change is important and who will benefit from it. Provide context for the change, such as highlighting relevant past work in this area or specific use cases.
19 | - type: textarea
20 | id: proposal
21 | validations:
22 | required: true
23 | attributes:
24 | label: '📝 Proposal'
25 | description: Describe the change that you are suggesting.
26 | placeholder: |
27 | Provide sufficient detail for reviewers to give concrete feedback.
28 |
29 | Including a proposed implementation is optional, but if you do, consider adding design details such as TypeScript examples, database schema, or sequence diagrams.
30 |
31 | If the change requires particular care when being rolled out, it can be helpful to include a plan for a phased release.
32 | - id: willing-to-submit-pr
33 | type: dropdown
34 | attributes:
35 | label: Are you willing to submit a PR?
36 | description: This is absolutely not required, but we are happy to guide you in the contribution process.
37 | options:
38 | - Undecided
39 | - Yes, and I have enough information to get started
40 | - Yes, but I would like some more guidance
41 | - No, but I'm happy to collaborate on a PR with someone else
42 | - No, I don't have time to work on this right now
43 | validations:
44 | required: true
45 |
--------------------------------------------------------------------------------
/frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/connectivityValidation.ts:
--------------------------------------------------------------------------------
1 | import { IWorkflowFullDefinition } from '../../../../types/workflow';
2 | import { getValidationMessage } from './i18n';
3 |
4 | /**
5 | * Validate node connectivity
6 | * @param workflowData Complete workflow definition data
7 | * @returns List of connectivity validation problems
8 | */
9 | export const validateNodeConnectivity = (workflowData: IWorkflowFullDefinition): string[] => {
10 | const problems: string[] = [];
11 |
12 | // Check if process design is empty
13 | if (workflowData.processSchema.nodeInfoList.length === 0) {
14 | problems.push(getValidationMessage('connectivity', 'processDesignEmpty'));
15 | }
16 |
17 | // Check if dissociative node exist, means start node has no output edge, end node has no input edge, common node missing input edge or output edge
18 | for (const node of workflowData.processSchema.nodeInfoList) {
19 | const outputEdgeList = workflowData.processSchema.edgeInfoList.filter((edge) => edge.sourceNodeId === node.id);
20 | const inputEdgeList = workflowData.processSchema.edgeInfoList.filter((edge) => edge.targetNodeId === node.id);
21 |
22 | if (node.type === 'start') {
23 | if (outputEdgeList.length === 0) {
24 | problems.push(getValidationMessage('connectivity', 'startNodeNoOutput'));
25 | }
26 | }
27 |
28 | if (node.type === 'end') {
29 | if (inputEdgeList.length === 0) {
30 | problems.push(getValidationMessage('connectivity', 'endNodeNoInput'));
31 | }
32 | }
33 |
34 | if (['normal', 'parallel', 'exclusive', 'timer', 'hook'].includes(node.type)) {
35 | if (outputEdgeList.length === 0 || inputEdgeList.length === 0) {
36 | problems.push(getValidationMessage('connectivity', 'nodeNoConnection', {
37 | nodeType: node.type
38 | }));
39 | }
40 | }
41 | }
42 |
43 | return problems;
44 | };
45 |
--------------------------------------------------------------------------------
/backend/tests/base.py:
--------------------------------------------------------------------------------
1 |
2 | class LoonflowApiCall(object):
3 | """
4 | loonflow api调用
5 | """
6 | def __init__(self, username='admin'):
7 | from service.common.common_service import common_service_ins
8 | flag, msg = common_service_ins.gen_signature('ops')
9 | if not flag:
10 | pass
11 | self.signature = msg.get('signature', '')
12 | self.timestamp = msg.get('timestamp', '')
13 | self.headers = {'HTTP_SIGNATURE': self.signature, 'HTTP_TIMESTAMP': self.timestamp, 'HTTP_APPNAME': 'ops', 'HTTP_USERNAME':username}
14 |
15 | def api_call(self, method, url, params={}):
16 | import json
17 | from django.test.client import Client
18 | c = Client()
19 | if method not in ('get', 'post', 'patch', 'delete', 'put'):
20 | return json.loads(dict(code=-1, msg='method is invalid'))
21 | if method == 'get':
22 | response_content = c.get(url, data=params, **self.headers).content
23 | elif method == 'post':
24 | # response_content = c.post(url, params, content_type='application/json', **self.headers).content
25 | response_content = c.post(url, data=json.dumps(params), content_type='application/json', **self.headers).content
26 | elif method == 'patch':
27 | response_content = c.patch(url, data=json.dumps(params), content_type='application/json', **self.headers).content
28 | elif method == 'delete':
29 | response_content = c.delete(url, data=json.dumps(params), content_type='application/json', **self.headers).content
30 | elif method == 'put':
31 | response_content = c.put(url, data=json.dumps(params), content_type='application/json', **self.headers).content
32 | if url == '/api/v1.0/accounts/app_token':
33 | print('#'*30)
34 | print(method)
35 | print(params)
36 | print(response_content)
37 | print('#' * 30)
38 | response_content_dict = json.loads(str(response_content, encoding='utf-8'))
39 |
40 | return response_content_dict
41 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/workflow/workflow_version.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/user_guide/workflow/workflow_version.rst:2
22 | msgid "Workflow Version Management"
23 | msgstr "流程版本管理"
24 |
25 | #: ../../source/user_guide/workflow/workflow_version.rst:4
26 | msgid ""
27 | "Loonflow supports multiple workflow versions. Each workflow can have the "
28 | "following version types:"
29 | msgstr "Loonflow 支持多版本流程。每个流程可拥有以下版本类型:"
30 |
31 | #: ../../source/user_guide/workflow/workflow_version.rst:7
32 | msgid ""
33 | "**default**: The active version used when a user selects the workflow in "
34 | "Workbench to create a new ticket."
35 | msgstr "**default**:当前生效版本,工作台选择流程创建工单时使用。"
36 |
37 | #: ../../source/user_guide/workflow/workflow_version.rst:9
38 | msgid ""
39 | "**candidate**: A draft version for validation. Administrators can switch "
40 | "the version type or create test tickets with the candidate version."
41 | msgstr "**candidate**:候选版本,用于验证。管理员可切换版本类型或用该版本创建测试工单。"
42 |
43 | #: ../../source/user_guide/workflow/workflow_version.rst:11
44 | msgid ""
45 | "**archived**: A retired version. New tickets cannot be created with it, "
46 | "but existing tickets created in the past can still be processed."
47 | msgstr "**archived**:归档版本,不能再创建新工单,但历史工单仍可按该版本继续处理。"
48 |
49 | #: ../../source/user_guide/workflow/workflow_version.rst:14
50 | msgid "workflow version management"
51 | msgstr "流程版本管理页面"
52 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r2.x/r2.0.0.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:2
22 | msgid "r2.0.0"
23 | msgstr "r2.0.0"
24 |
25 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:4
26 | msgid ""
27 | "Built-in ticket creation, viewing, processing, and management interface "
28 | "(major change in this release)"
29 | msgstr "内置工单创建、查看、处理、管理界面(本次版本重大变更)"
30 |
31 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:5
32 | msgid ""
33 | "Support for users belonging to multiple departments simultaneously "
34 | "(significant change in this release)"
35 | msgstr "支持用户同时隶属多个部门(本次版本重大变更)"
36 |
37 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:6
38 | msgid "flowlog API supports specifying order or reverse order"
39 | msgstr "flowlog 接口支持指定正序或倒序"
40 |
41 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:7
42 | msgid ""
43 | "Workflow configuration interface supports viewing daily new ticket "
44 | "statistics"
45 | msgstr "流程配置界面支持查看每日新增工单统计"
46 |
47 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:8
48 | msgid "Ticket details support administrator intervention"
49 | msgstr "工单详情支持管理员干预"
50 |
51 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:9
52 | msgid "Support for users to change their own passwords"
53 | msgstr "支持用户修改自己的密码"
54 |
55 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:10
56 | msgid "Various other optimizations"
57 | msgstr "其他多项优化"
58 |
59 |
--------------------------------------------------------------------------------
/frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/exclusiveNodeValidation.ts:
--------------------------------------------------------------------------------
1 | import { IWorkflowFullDefinition } from '../../../../types/workflow';
2 | import { getValidationMessage } from './i18n';
3 |
4 | /**
5 | * Validate exclusive gateway nodes
6 | * @param workflowData Complete workflow definition data
7 | * @returns List of exclusive gateway validation problems
8 | */
9 | export const validateExclusiveNodes = (workflowData: IWorkflowFullDefinition): string[] => {
10 | const problems: string[] = [];
11 |
12 | for (const node of workflowData.processSchema.nodeInfoList) {
13 | if (node.type === 'exclusive') {
14 | // 获取排他网关的入边和出边
15 | const inputEdges = workflowData.processSchema.edgeInfoList.filter(
16 | (edge) => edge.targetNodeId === node.id
17 | );
18 | const outputEdges = workflowData.processSchema.edgeInfoList.filter(
19 | (edge) => edge.sourceNodeId === node.id
20 | );
21 |
22 | // 检查排他网关必须有入边和出边
23 | if (inputEdges.length === 0) {
24 | problems.push(getValidationMessage('exclusive', 'noInputEdge', {
25 | nodeName: node.name
26 | }));
27 | }
28 | if (outputEdges.length === 0) {
29 | problems.push(getValidationMessage('exclusive', 'noOutputEdge', {
30 | nodeName: node.name
31 | }));
32 | }
33 |
34 | // 检查排他网关的起始节点不可以是结束类型的节点
35 | for (const inputEdge of inputEdges) {
36 | const sourceNode = workflowData.processSchema.nodeInfoList.find(
37 | (n) => n.id === inputEdge.sourceNodeId
38 | );
39 | if (sourceNode && sourceNode.type === 'end') {
40 | problems.push(getValidationMessage('exclusive', 'sourceNodeCannotBeEnd', {
41 | nodeName: node.name,
42 | sourceNodeName: sourceNode.name
43 | }));
44 | }
45 | }
46 | }
47 | }
48 |
49 | return problems;
50 | };
51 |
--------------------------------------------------------------------------------
/backend/tests/test_models/test_account_model.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from apps.account.models import User, Dept, Role
3 |
4 |
5 | class TestAccountModel(TestCase):
6 | fixtures = ['accounts.json']
7 |
8 | def test_loon_user_get_dict(self):
9 | """
10 | 测试获取字典格式用户信息
11 | :return:
12 | """
13 | test_user1 = User.objects.get(username='guiji')
14 | assert isinstance(test_user1.get_dict(), dict)
15 |
16 | def test_loon_user_is_staff(self):
17 | """
18 | 测试用户staff判断
19 | :return:
20 | """
21 | test_user1 = User.objects.get(username='guiji')
22 | assert isinstance(test_user1.is_staff, bool)
23 |
24 | def test_loon_user_get_short_name(self):
25 | """
26 | 测试获取short_name
27 | :return:
28 | """
29 | test_user1 = User.objects.get(username='guiji')
30 | self.assertEqual(test_user1.get_short_name(), 'guiji')
31 |
32 | def test_loon_user_get_alias_name(self):
33 | """
34 | 测试获取用户昵称
35 | :return:
36 | """
37 | test_user1 = User.objects.get(username='guiji')
38 | self.assertEqual(test_user1.get_alias_name(), '轨迹')
39 |
40 | def test_loon_user_dept_name(self):
41 | """
42 | 测试获取用户部门名称
43 | :return:
44 | """
45 | test_user1 = User.objects.get(username='admin')
46 | self.assertEqual(test_user1.dept_name, '总部,技术部')
47 |
48 | def test_loon_user_get_json(self):
49 | """
50 | 测试获取用户json格式信息
51 | :return:
52 | """
53 | test_user1 = User.objects.get(username='guiji')
54 | assert isinstance(test_user1.get_json(), str)
55 |
56 | def test_role_get_dict(self):
57 | """
58 | 测试获取角色字典信息格式
59 | :return:
60 | """
61 | role = LoonRole.objects.get(name='VPN管理员')
62 | assert isinstance(role.get_dict(), dict)
63 |
64 | def test_dept_get_dict(self):
65 | """
66 | 测试获取部门字典信息格式
67 | :return:
68 | """
69 | dept = LoonDept.objects.get(id=1)
70 | assert isinstance(dept.get_dict(), dict)
71 |
--------------------------------------------------------------------------------
/sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/setting/tenant.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) 2020-2025, blackholll
3 | # This file is distributed under the same license as the loonflow package.
4 | # FIRST AUTHOR , 2025.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: loonflow \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n"
11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n"
12 | "Last-Translator: ChatGPT \n"
13 | "Language: zh_CN\n"
14 | "Language-Team: zh_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0;\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.17.0\n"
20 |
21 | #: ../../source/user_guide/setting/tenant.rst:2
22 | msgid "Tenant Management"
23 | msgstr "租户管理"
24 |
25 | #: ../../source/user_guide/setting/tenant.rst:4
26 | msgid ""
27 | "Loonflow supports multi-tenant. Most users only need a single tenant; if "
28 | "you require multi-tenant capabilities, please contact blackholll "
29 | "(blackholll@163.com; blackholll_cn@gmail.com) for licensing. In multi-"
30 | "tenant mode all data—users, tickets, workflows, notifications, "
31 | "applications, roles, departments, and more—are fully isolated. Typical "
32 | "scenarios include group enterprises or SaaS deployments where multiple "
33 | "independent organizations share one platform."
34 | msgstr "Loonflow 支持多租户。大多数用户只需单租户;如需多租户能力,请联系 blackholll(blackholll@163.com;blackholll_cn@gmail.com)获取授权。多租户模式下,用户、工单、流程、通知、应用、角色、部门等数据完全隔离,适用于集团企业或多组织共用平台的 SaaS 场景。"
35 |
36 | #: ../../source/user_guide/setting/tenant.rst:6
37 | msgid "tenant info"
38 | msgstr "租户信息"
39 |
40 | #: ../../source/user_guide/setting/tenant.rst:12
41 | msgid "Future roadmap"
42 | msgstr "后续规划"
43 |
44 | #: ../../source/user_guide/setting/tenant.rst:13
45 | msgid ""
46 | "Planned: cross-tenant ticket collaboration, allowing tickets to flow "
47 | "between tenants. Example scenarios: inter-company ticket handling, "
48 | "external customer cases, supplier tickets, and reseller/partner support."
49 | msgstr "规划中:跨租户工单协同,支持工单在租户间流转,适用于跨公司工单处理、外部客户案例、供应商工单、渠道伙伴支持等场景。"
50 |
--------------------------------------------------------------------------------
/sphinx_docs/source/others/release_notes/r3.x/r3.0.0.rst:
--------------------------------------------------------------------------------
1 | r3.0.0
2 | ---------
3 |
4 | 🎯 Revolutionary Visual Design
5 |
6 | - **Drag-and-Drop Process Designer**: No complex configuration needed. Complete complex business process modeling through intuitive drag-and-drop connections. Supports advanced nodes like conditional branches, parallel tasks, and hooks.
7 | - **Smart Form Designer**: Powerful visual form building tool with rich field types (text, numbers, dropdowns, personnel selection, attachments, etc.) and flexible layouts.
8 | - **Real-time Preview & Validation**: Real-time preview during process design with built-in process logic validation to prevent design errors early.
9 | - **Multi-Version Process Configuration**: You can configure multiple versions of processes and easily test and switch between versions.
10 |
11 |
12 | 🔧 Ultimate Flexibility & Extensibility
13 |
14 | - **Plugin Architecture**: We provide plugin extension capabilities for almost all key nodes (such as custom actions, permission validation, notification methods, etc.). Your unique business logic can be easily integrated like building blocks.
15 | - **Powerful API System**: Provides comprehensive and clear RESTful APIs for seamless integration with your customer service systems, CMDB, monitoring systems, CI/CD, OA, and other third-party systems.
16 | - **Highly Customizable Permission Model**: Supports fine-grained permission control based on roles, departments, or even specific business conditions to meet complex enterprise permission management needs.
17 |
18 | 💼 Enterprise-Ready Features Out of the Box
19 |
20 | - **Multi-Type Ticket Support**: Easily manage various processes including IT operations, HR approvals, financial reimbursements, customer service, etc.
21 | - **Automation & Smart Routing**: Supports conditional routing based on form data, automatic assignee assignment, and intelligent ticket flow.
22 | - **Comprehensive Audit Logs**: Complete records of every operation from ticket creation to closure, meeting compliance and audit requirements.
23 | - **Multi-Tenant Support (Optional)**: Provides data isolation capabilities for SaaS providers or large enterprise groups (requires additional authorization).
24 |
--------------------------------------------------------------------------------
/backend/service/permission/user_permission.py:
--------------------------------------------------------------------------------
1 | import json
2 | from functools import wraps
3 | from service.account.account_application_service import account_application_service_ins
4 | from service.account.account_base_service import account_base_service_ins
5 | from service.account.account_user_service import account_user_service_ins
6 | from service.exception.custom_common_exception import CustomCommonException
7 | from service.format_response import api_response
8 |
9 |
10 | def user_permission_check(type='', workflow_id_source='url'):
11 | """
12 | user permission check decorator
13 | :param workflow_id_source: from url or body
14 | :param types:
15 | :return:
16 | """
17 | def decorator(func):
18 | @wraps(func)
19 | def _deco(view_class, request, *args, **kwargs):
20 | email = request.META.get('HTTP_EMAIL')
21 | tenant_id = request.META.get('HTTP_TENANTID')
22 | app_name = request.META.get('HTTP_APPNAME')
23 | if workflow_id_source == 'url':
24 | workflow_id = kwargs.get('workflow_id')
25 | workflow_version_id = kwargs.get('workflow_version_id')
26 | elif workflow_id_source == 'body':
27 | request_data = request.body
28 | workflow_id = json.loads(request_data).get('workflow_id')
29 | workflow_version_id = json.loads(request_data).get('workflow_version_id')
30 | if app_name != 'loonflow':
31 | try:
32 | account_application_service_ins.app_type_check(tenant_id, app_name, type, workflow_id, workflow_version_id)
33 | except CustomCommonException as e:
34 | return api_response(-1, 'has no permission: {}'.format(e.message), {})
35 | else:
36 | try:
37 | account_user_service_ins.user_type_check(email=email, tenant_id=tenant_id, type=type)
38 | except CustomCommonException as e:
39 | return api_response(-1, 'has no permission: {}'.format(e.message), {})
40 | return func(view_class, request, *args, **kwargs)
41 | return _deco
42 | return decorator
43 |
44 |
--------------------------------------------------------------------------------
/frontend/src/services/role.ts:
--------------------------------------------------------------------------------
1 | import apiClient from './api';
2 |
3 | export const getRoleList = async (searchValue: string, page: number, perPage: number) => {
4 | const response = await apiClient.get('/api/v1.0/accounts/roles', { params: { search_value: searchValue, page: page + 1, per_page: perPage } });
5 | return response.data;
6 | };
7 | export const getSimpleRoles = async (searchValue: string, roleIds: string, page: number, perPage: number) => {
8 | const response = await apiClient.get('/api/v1.0/accounts/simple_roles', { params: { search_value: searchValue, role_ids: roleIds, page: page + 1, per_page: perPage } });
9 | return response.data;
10 | };
11 | export const deleteRole = async (roleId: string) => {
12 | const response = await apiClient.delete(`/api/v1.0/accounts/roles/${roleId}`);
13 | return response.data;
14 | };
15 |
16 | export const addRole = async (name: string, description: string) => {
17 | const response = await apiClient.post('/api/v1.0/accounts/roles', { name, description });
18 | return response.data;
19 | };
20 | export const updateRole = async (roleId: string, name: string, description: string) => {
21 | const response = await apiClient.patch(`/api/v1.0/accounts/roles/${roleId}`, { name, description });
22 | return response.data;
23 | }
24 |
25 | export const getRoleDetail = async (roleId: string) => {
26 | const response = await apiClient.get(`/api/v1.0/accounts/roles/${roleId}`);
27 | return response.data;
28 | };
29 |
30 | export const addRoleUser = async (roleId: string, userIds: string[]) => {
31 | const response = await apiClient.post(`/api/v1.0/accounts/roles/${roleId}/users`, { user_ids: userIds });
32 | return response.data;
33 | }
34 |
35 | export const deleteRoleUser = async (roleId: string, userIds: string[]) => {
36 | const response = await apiClient.delete(`/api/v1.0/accounts/roles/${roleId}/users`, { data: { user_ids: userIds } });
37 | return response.data;
38 | }
39 |
40 | export const getRoleUserList = async (roleId: string, searchValue: string, page: number, perPage: number) => {
41 | const response = await apiClient.get(`/api/v1.0/accounts/roles/${roleId}/users`, { params: { search_value: searchValue, page: page + 1, per_page: perPage } });
42 | return response.data;
43 | }
--------------------------------------------------------------------------------