Merge branch 'develop' into 2.0

This commit is contained in:
katherinehhh 2025-06-27 14:28:41 +08:00
commit b44248f214
16 changed files with 767 additions and 298 deletions

View File

@ -274,7 +274,7 @@ jobs:
- run: npx playwright install chromium --with-deps
- name: Test with postgres
run: yarn e2e p-test --match 'packages/**/{plugin-workflow,plugin-workflow-*}/**/__e2e__/**/*.test.ts' --ignore 'packages/**/plugin-workflow-approval/**/__e2e__/**/*.test.ts'
run: yarn e2e p-test --match 'packages/**/{plugin-workflow,plugin-workflow-*}/**/__e2e__/**/*.test.ts' --ignore 'packages/**/plugin-workflow-approval/**/__e2e__/**/*.test.ts' --ignore 'packages/**/plugin-workflow-manual/**/__e2e__/**/*.test.ts'
env:
__E2E__: true
APP_ENV: production
@ -297,131 +297,131 @@ jobs:
timeout-minutes: 60
plugin-workflow-approval:
name: plugin-workflow-approval
needs: build
runs-on: ubuntu-latest
container: node:20
services:
# Label used to access the service container
postgres:
# Docker Hub image
image: postgres:11
# Provide the password for postgres
env:
POSTGRES_USER: nocobase
POSTGRES_PASSWORD: password
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,plugin-workflow-approval
skip-token-revoke: true
- uses: actions/checkout@v4
- name: Checkout pro-plugins
continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
uses: actions/checkout@v4
with:
repository: nocobase/pro-plugins
ref: main
path: packages/pro-plugins
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Checkout plugin-workflow-approval
continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
uses: actions/checkout@v4
with:
repository: nocobase/plugin-workflow-approval
ref: main
path: packages/pro-plugins/@nocobase/plugin-workflow-approval
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- run: |
cd packages/pro-plugins &&
if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then
git checkout ${{ github.head_ref || github.ref_name }}
else
if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then
git checkout ${{ github.event.pull_request.base.ref }}
else
git checkout main
fi
fi
cd ../../
cd packages/pro-plugins/@nocobase/plugin-workflow-approval &&
if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then
git checkout ${{ github.head_ref || github.ref_name }}
else
if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then
git checkout ${{ github.event.pull_request.base.ref }}
else
git checkout main
fi
fi
cd ../../../../
continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
- name: Git logs
run: |
cd packages/pro-plugins/@nocobase/plugin-workflow-approval && git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -n 10
continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
- name: Set variables
continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
run: |
APPEND_PRESET_LOCAL_PLUGINS=$(find ./packages/pro-plugins/@nocobase -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed 's/^plugin-//' | tr '\n' ',' | sed 's/,$//')
echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT
id: vars
# plugin-workflow-approval:
# name: plugin-workflow-approval
# needs: build
# runs-on: ubuntu-latest
# container: node:20
# services:
# # Label used to access the service container
# postgres:
# # Docker Hub image
# image: postgres:11
# # Provide the password for postgres
# env:
# POSTGRES_USER: nocobase
# POSTGRES_PASSWORD: password
# # Set health checks to wait until postgres has started
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# steps:
# - uses: actions/create-github-app-token@v1
# id: app-token
# with:
# app-id: ${{ vars.NOCOBASE_APP_ID }}
# private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
# repositories: nocobase,pro-plugins,plugin-workflow-approval
# skip-token-revoke: true
# - uses: actions/checkout@v4
# - name: Checkout pro-plugins
# continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
# uses: actions/checkout@v4
# with:
# repository: nocobase/pro-plugins
# ref: main
# path: packages/pro-plugins
# fetch-depth: 0
# token: ${{ steps.app-token.outputs.token }}
# - name: Checkout plugin-workflow-approval
# continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
# uses: actions/checkout@v4
# with:
# repository: nocobase/plugin-workflow-approval
# ref: main
# path: packages/pro-plugins/@nocobase/plugin-workflow-approval
# fetch-depth: 0
# token: ${{ steps.app-token.outputs.token }}
# - run: |
# cd packages/pro-plugins &&
# if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then
# git checkout ${{ github.head_ref || github.ref_name }}
# else
# if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then
# git checkout ${{ github.event.pull_request.base.ref }}
# else
# git checkout main
# fi
# fi
# cd ../../
# cd packages/pro-plugins/@nocobase/plugin-workflow-approval &&
# if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then
# git checkout ${{ github.head_ref || github.ref_name }}
# else
# if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then
# git checkout ${{ github.event.pull_request.base.ref }}
# else
# git checkout main
# fi
# fi
# cd ../../../../
# continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
# - name: Git logs
# run: |
# cd packages/pro-plugins/@nocobase/plugin-workflow-approval && git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -n 10
# continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
# - name: Set variables
# continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true
# run: |
# APPEND_PRESET_LOCAL_PLUGINS=$(find ./packages/pro-plugins/@nocobase -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed 's/^plugin-//' | tr '\n' ',' | sed 's/,$//')
# echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT
# id: vars
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
# - name: Get yarn cache directory path
# id: yarn-cache-dir-path
# run: echo "::set-output name=dir::$(yarn cache dir)"
# - uses: actions/cache@v4
# id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
# with:
# path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
# key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
# restore-keys: |
# ${{ runner.os }}-yarn-
- run: yarn
# - run: yarn
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: build-artifact
path: packages
# - name: Download build artifact
# uses: actions/download-artifact@v4
# with:
# name: build-artifact
# path: packages
- run: npx playwright install chromium --with-deps
- name: Test with postgres
run: yarn e2e p-test --match 'packages/**/plugin-workflow-approval/**/__e2e__/**/*.test.ts'
env:
__E2E__: true
APP_ENV: production
LOGGER_LEVEL: error
DB_DIALECT: postgres
DB_HOST: postgres
DB_PORT: 5432
DB_USER: nocobase
DB_PASSWORD: password
DB_DATABASE: nocobase
APPEND_PRESET_LOCAL_PLUGINS: ${{ steps.vars.outputs.var2 }}
ENCRYPTION_FIELD_KEY: 1%&glK;<UA}aIxJVc53-4G(rTi0vg@J]
# - run: npx playwright install chromium --with-deps
# - name: Test with postgres
# run: yarn e2e p-test --match 'packages/**/plugin-workflow-approval/**/__e2e__/**/*.test.ts'
# env:
# __E2E__: true
# APP_ENV: production
# LOGGER_LEVEL: error
# DB_DIALECT: postgres
# DB_HOST: postgres
# DB_PORT: 5432
# DB_USER: nocobase
# DB_PASSWORD: password
# DB_DATABASE: nocobase
# APPEND_PRESET_LOCAL_PLUGINS: ${{ steps.vars.outputs.var2 }}
# ENCRYPTION_FIELD_KEY: 1%&glK;<UA}aIxJVc53-4G(rTi0vg@J]
- name: Upload e2e-report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: e2e-report-${{ github.job }} # 为了防止在多个任务中存在冲突
path: ./storage/playwright/tests-report-blob/blob-*/*
# - name: Upload e2e-report
# if: ${{ !cancelled() }}
# uses: actions/upload-artifact@v4
# with:
# name: e2e-report-${{ github.job }} # 为了防止在多个任务中存在冲突
# path: ./storage/playwright/tests-report-blob/blob-*/*
timeout-minutes: 180
# timeout-minutes: 180
plugin-data-source-main:
name: plugin-data-source-main

View File

@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.7.18](https://github.com/nocobase/nocobase/compare/v1.7.17...v1.7.18) - 2025-06-26
### 🚀 Improvements
- **[Workflow]** Optimize mobile style ([#7040](https://github.com/nocobase/nocobase/pull/7040)) by @mytharcher
- **[Public forms]** Optimize the performance of date components in public forms ([#7117](https://github.com/nocobase/nocobase/pull/7117)) by @zhangzhonghe
### 🐛 Bug Fixes
- **[Workflow]** Fix params of loading record in tasks ([#7123](https://github.com/nocobase/nocobase/pull/7123)) by @mytharcher
- **[WEB client]** Fix issue where blocks under pages were not displayed after setting role menu permissions ([#7112](https://github.com/nocobase/nocobase/pull/7112)) by @aaaaaajie
- **[Workflow: Approval]**
- Fix applicant variable name in trigger by @mytharcher
- Fix mobile styles by @mytharcher
- Fix error thrown when approval related collection deleted by @mytharcher
## [v1.7.17](https://github.com/nocobase/nocobase/compare/v1.7.16...v1.7.17) - 2025-06-23
### 🐛 Bug Fixes

View File

@ -5,6 +5,27 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
## [v1.7.18](https://github.com/nocobase/nocobase/compare/v1.7.17...v1.7.18) - 2025-06-26
### 🚀 优化
- **[工作流]** 优化移动端样式 ([#7040](https://github.com/nocobase/nocobase/pull/7040)) by @mytharcher
- **[公开表单]** 优化公开表单中日期组件的性能 ([#7117](https://github.com/nocobase/nocobase/pull/7117)) by @zhangzhonghe
### 🐛 修复
- **[工作流]** 修复待办中心加载记录的参数 ([#7123](https://github.com/nocobase/nocobase/pull/7123)) by @mytharcher
- **[WEB 客户端]** 修复设置角色菜单权限后页面下区块不显示的问题 ([#7112](https://github.com/nocobase/nocobase/pull/7112)) by @aaaaaajie
- **[工作流:审批]**
- 修复审批触发器中申请人变量名的问题 by @mytharcher
- 修复移动端样式 by @mytharcher
- 修复审批关联表被删除后的报错 by @mytharcher
## [v1.7.17](https://github.com/nocobase/nocobase/compare/v1.7.16...v1.7.17) - 2025-06-23
### 🐛 修复

View File

@ -19,6 +19,7 @@ import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive'
import { useSchemaComponentContext } from '../../hooks';
import { AssociationFieldContext } from './context';
import { FormItem, useSchemaOptionsContext } from '../../../schema-component';
import { useCollectionRecord } from '../../../data-source';
export const AssociationFieldProvider = observer(
(props) => {
@ -28,6 +29,7 @@ export const AssociationFieldProvider = observer(
const api = useAPIClient();
const option = useSchemaOptionsContext();
const rootRef = useRef<HTMLDivElement>(null);
const record = useCollectionRecord();
// 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染
useSchemaComponentContext();
@ -71,7 +73,9 @@ export const AssociationFieldProvider = observer(
if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) {
return Promise.reject(null);
}
if (record && !record.isNew) {
return Promise.reject(null);
}
return api.request({
resource: collectionField.target,
action: Array.isArray(ids) ? 'list' : 'get',

View File

@ -48,11 +48,11 @@ const useStyles = genStyleHook('nb-list', (token) => {
width: '100%',
flexDirection: 'column',
'&:not(:first-child)': {
paddingTop: token.paddingContentVertical,
marginTop: token.paddingContentVertical,
},
'&:not(:last-child)': {
paddingBottom: token.paddingContentVertical,
marginBottom: token.paddingContentVertical,
borderBottom: `1px solid ${token.colorBorderSecondary}`,
},
},

View File

@ -244,7 +244,7 @@ function FinallyButton({
inheritsCollections,
linkageFromForm,
allowAddToCurrent,
props,
props: { onlyIcon, ...props },
componentType,
menu,
onClick,
@ -362,7 +362,7 @@ function FinallyButton({
...buttonStyle,
}}
>
{props.onlyIcon ? props?.children?.[1] : props?.children}
{onlyIcon ? props?.children?.[1] : props?.children}
</Button>
);
}

View File

@ -64,7 +64,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props:
const localVariables = useLocalVariables();
const collection = useCollection_deprecated();
const record = useRecord();
const { form } = useFormBlockContext();
const { form, type } = useFormBlockContext();
const { getFields } = useCollectionFilterOptionsV2(collection);
const { isInSubForm, isInSubTable } = useFlag() || {};
@ -219,7 +219,6 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props:
targetField,
variables,
]);
const handleSubmit: (values: any) => void = useCallback(
(v) => {
const schema: ISchema = {
@ -227,7 +226,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props:
};
fieldSchema.default = v.default ?? null;
if (!isVariable(v.default)) {
field.setInitialValue?.(v.default);
(record.__isNewRecord__ || type === 'create') && field.setInitialValue?.(v.default);
}
schema.default = v.default ?? null;
dn.emit('patch', {

View File

@ -84,8 +84,8 @@ export const actionDesignerCss = css`
`;
export const DuplicateAction = observer(
(props: any) => {
const { children, onlyIcon, icon, title, ...others } = props;
({ onlyIcon, ...props }: any) => {
const { children, icon, title, ...others } = props;
const { message } = App.useApp();
const field = useField();
const fieldSchema = useFieldSchema();

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Model, Transaction } from '@nocobase/database';
import { Model, MultipleRelationRepository, Transaction } from '@nocobase/database';
import PluginLocalizationServer from '@nocobase/plugin-localization';
import { Plugin } from '@nocobase/server';
import { tval } from '@nocobase/utils';
@ -216,7 +216,7 @@ export class PluginClientServer extends Plugin {
const tabIds = tabs.map((x) => x.get('id'));
const where = { desktopRouteId: tabIds, roleName };
if (action === 'create') {
const exists = await repository.find({ where });
const exists = await repository.find({ where, transaction });
const modelsByRouteId = _.keyBy(exists, (x) => x.get('desktopRouteId'));
const createModels = tabs
.map((x) => !modelsByRouteId[x.get('id')] && { desktopRouteId: x.get('id'), roleName })
@ -282,6 +282,24 @@ export class PluginClientServer extends Plugin {
await next();
});
this.app.resourceManager.registerActionHandler('roles.desktopRoutes:set', async (ctx, next) => {
let { values } = ctx.action.params;
if (values.length) {
const instances = await this.app.db.getRepository('desktopRoutes').find({
filter: {
$or: [{ id: { $in: values } }, { parentId: { $in: values } }],
},
});
values = instances.map((instance) => instance.get('id'));
}
const { resourceName, sourceId } = ctx.action;
const repository = this.app.db.getRepository<MultipleRelationRepository>(resourceName, sourceId);
await repository['set'](values);
ctx.status = 200;
await next();
});
}
registerLocalizationSource() {

View File

@ -10,9 +10,14 @@
import { useToken } from '@nocobase/client';
import _ from 'lodash';
import React, { FC, useEffect } from 'react';
import classnames from 'classnames';
import { PageBackgroundColor } from '../../../constants';
export const MobilePageContentContainer: FC<{ hideTabBar?: boolean; displayPageHeader?: boolean }> = ({ children, hideTabBar, displayPageHeader = true }) => {
export const MobilePageContentContainer: FC<{
hideTabBar?: boolean;
displayPageHeader?: boolean;
className?: string;
}> = ({ children, hideTabBar, displayPageHeader = true, className }) => {
const [mobileTabBarHeight, setMobileTabBarHeight] = React.useState(0);
const [mobilePageHeader, setMobilePageHeader] = React.useState(0);
const { token } = useToken();
@ -29,9 +34,9 @@ export const MobilePageContentContainer: FC<{ hideTabBar?: boolean; displayPageH
});
return (
<>
{(mobilePageHeader && displayPageHeader) ? <div style={{ height: mobilePageHeader }}></div> : null}
{mobilePageHeader && displayPageHeader ? <div style={{ height: mobilePageHeader }}></div> : null}
<div
className="mobile-page-content"
className={classnames('mobile-page-content', className)}
data-testid="mobile-page-content"
style={{
height: `calc(100% - ${(mobileTabBarHeight || 0) + (mobilePageHeader || 0)}px)`,

View File

@ -17,6 +17,8 @@ import {
useRequest,
useResourceActionContext,
useResourceContext,
useFilterFieldProps,
useFilterFieldOptions,
} from '@nocobase/client';
import React from 'react';
import { i18nText } from '../../utils';
@ -212,6 +214,16 @@ export const tableActionColumnSchema: ISchema = {
},
};
export const useFilterActionProps = () => {
const { collection } = useResourceContext();
const options = useFilterFieldOptions(collection.fields);
const service = useResourceActionContext();
return useFilterFieldProps({
options: options,
params: service.state?.params?.[0] || service.params,
service,
});
};
export const schema: ISchema = {
type: 'object',
properties: {
@ -245,6 +257,18 @@ export const schema: ISchema = {
},
},
properties: {
filter: {
'x-component': 'Filter.Action',
'x-use-component-props': useFilterActionProps,
default: {
$and: [{ displayName: { $includes: '' } }, { name: { $includes: '' } }],
},
title: "{{t('Filter')}}",
'x-component-props': {
icon: 'FilterOutlined',
},
'x-align': 'left',
},
delete: {
type: 'void',
title: '{{ t("Delete") }}',

View File

@ -7,10 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React, { useState, useCallback } from 'react';
import { DatePicker } from 'antd-mobile';
import { mapDatePicker, DatePicker as NBDatePicker } from '@nocobase/client';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { mapDatePicker, DatePicker as NBDatePicker } from '@nocobase/client';
import { DatePicker } from 'antd-mobile';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
function getPrecision(timeFormat: string): 'hour' | 'minute' | 'second' {
@ -90,8 +90,8 @@ const MobileDateTimePicker = connect(
}}
precision={showTime ? getPrecision(timeFormat) : picker === 'date' ? 'day' : picker}
renderLabel={labelRenderer}
min={new Date(1000, 0, 1)}
max={new Date(9999, 11, 31)}
min={new Date(1950, 0, 1)}
max={new Date(2050, 11, 31)}
onConfirm={(val) => {
handleConfirm(val);
}}

View File

@ -40,6 +40,7 @@ import {
ActionContextProvider,
useRequest,
CollectionRecordProvider,
useMobileLayout,
} from '@nocobase/client';
import WorkflowPlugin, {
DetailsBlockProvider,
@ -56,6 +57,7 @@ import { NAMESPACE, useLang } from '../locale';
import { FormBlockProvider } from './instruction/FormBlockProvider';
import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig';
import { TaskStatusOptionsMap, TASK_STATUS } from '../common/constants';
import { useMobilePage } from '@nocobase/plugin-mobile/client';
function TaskStatusColumn(props) {
const recordData = useCollectionRecordData();
@ -390,7 +392,7 @@ function FlowContextProvider(props) {
}}
schema={{
type: 'void',
name: 'tabs',
name: `manual-${id}}`,
'x-component': 'Tabs',
properties: node.config?.schema,
}}
@ -429,16 +431,23 @@ function useDetailsBlockProps() {
}
function FooterStatus() {
const { isMobileLayout } = useMobileLayout();
const mobilePage = useMobilePage();
const compile = useCompile();
const { status, updatedAt } = useCollectionRecordData() || {};
const statusOption = TaskStatusOptionsMap[status];
const isMobile = Boolean(mobilePage || isMobileLayout);
return status ? (
<Space
className={css`
margin-bottom: 1em;
padding: ${isMobileLayout ? '0 1em' : '0'};
margin-bottom: ${isMobile ? '0' : '1em'};
time {
margin-right: 0.5em;
}
.ant-tag {
margin-right: 0;
}
`}
>
<time>{dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
@ -449,9 +458,10 @@ function FooterStatus() {
function Drawer() {
const ctx = useContext(SchemaComponentContext);
const { id, node, workflow, status } = useCollectionRecordData() || {};
const record = useCollectionRecordData();
const { id, node, workflow, status } = record || {};
return (
return record ? (
<SchemaComponentContext.Provider value={{ ...ctx, reset() {}, designable: false }}>
<SchemaComponent
components={{
@ -460,7 +470,7 @@ function Drawer() {
}}
schema={{
type: 'void',
name: `drawer-${id}-${status}`,
name: `manual-detail-drawer-${id}-${status}`,
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
@ -485,7 +495,7 @@ function Drawer() {
}}
/>
</SchemaComponentContext.Provider>
);
) : null;
}
function Decorator(props) {
@ -667,6 +677,8 @@ function useTodoActionParams(status) {
return {
filter,
appends: [
'node.id',
'node.title',
'job.id',
'job.status',
'job.result',
@ -676,10 +688,11 @@ function useTodoActionParams(status) {
'execution.id',
'execution.status',
],
except: ['node.config', 'workflow.config', 'workflow.options'],
};
}
function TodoExtraActions() {
function TodoExtraActions(props) {
return (
<SchemaComponent
schema={{
@ -694,6 +707,7 @@ function TodoExtraActions() {
'x-use-component-props': 'useRefreshActionProps',
'x-component-props': {
icon: 'ReloadOutlined',
...props,
},
},
filter: {
@ -703,6 +717,7 @@ function TodoExtraActions() {
'x-use-component-props': 'useFilterActionProps',
'x-component-props': {
icon: 'FilterOutlined',
...props,
},
default: {
$and: [{ title: { $includes: '' } }, { 'workflow.title': { $includes: '' } }],

View File

@ -40,6 +40,7 @@
"@nocobase/logger": "1.x",
"@nocobase/plugin-data-source-main": "1.x",
"@nocobase/plugin-error-handler": "1.x",
"@nocobase/plugin-mobile": "1.x",
"@nocobase/plugin-users": "1.x",
"@nocobase/resourcer": "1.x",
"@nocobase/server": "1.x",

View File

@ -6,12 +6,14 @@
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CheckCircleOutlined } from '@ant-design/icons';
import { CheckCircleOutlined, EllipsisOutlined } from '@ant-design/icons';
import { PageHeader } from '@ant-design/pro-layout';
import { Badge, Button, Layout, Menu, Tabs, Tooltip } from 'antd';
import { Badge, Button, Flex, Layout, Menu, Popover, Segmented, Tabs, theme, Tooltip } from 'antd';
import classnames from 'classnames';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { NavBar, Toast } from 'antd-mobile';
import { observer } from '@formily/react';
import {
ActionContextProvider,
@ -26,11 +28,25 @@ import {
useCompile,
useDocumentTitle,
useIsLoggedIn,
useMobileLayout,
usePlugin,
useRequest,
useToken,
SchemaInitializerItemType,
APIClient,
} from '@nocobase/client';
import {
MobilePageContentContainer,
MobilePageHeader,
MobilePageNavigationBar,
MobilePageProvider,
MobileRouteItem,
MobileTabBarItem,
useMobilePage,
useMobileRoutes,
} from '@nocobase/plugin-mobile/client';
import PluginWorkflowClient from '.';
import { lang, NAMESPACE } from './locale';
@ -39,11 +55,6 @@ const layoutClass = css`
overflow: hidden;
`;
const contentClass = css`
min-height: 280px;
overflow: auto;
`;
export interface TaskTypeOptions {
title: string;
collection: string;
@ -52,6 +63,7 @@ export interface TaskTypeOptions {
Actions?: React.ComponentType;
Item: React.ComponentType;
Detail: React.ComponentType;
getPopupRecord?: (apiClient: APIClient, { params }: { params: any }) => Promise<any>;
// children?: TaskTypeOptions[];
alwaysShow?: boolean;
}
@ -70,12 +82,18 @@ function MenuLink({ type }: any) {
const { title } = workflowPlugin.taskTypes.get(type);
const { counts } = useContext(TasksCountsContext);
const typeTitle = compile(title);
const mobilePage = useMobilePage();
return (
<Link
to={`/admin/workflow/tasks/${type}/${TASK_STATUS.PENDING}`}
to={
mobilePage
? `/page/workflow/tasks/${type}/${TASK_STATUS.PENDING}`
: `/admin/workflow/tasks/${type}/${TASK_STATUS.PENDING}`
}
className={css`
display: flex;
gap: 0.5em;
align-items: center;
justify-content: space-between;
width: 100%;
@ -103,13 +121,42 @@ function StatusTabs() {
const navigate = useNavigate();
const { taskType, status = TASK_STATUS.PENDING } = useParams();
const type = useCurrentTaskType();
const { isMobileLayout } = useMobileLayout();
const mobilePage = useMobilePage();
const onSwitchTab = useCallback(
(key: string) => {
navigate(mobilePage ? `/page/workflow/tasks/${taskType}/${key}` : `/admin/workflow/tasks/${taskType}/${key}`);
},
[navigate, taskType, mobilePage],
);
const isMobile = Boolean(mobilePage || isMobileLayout);
const { Actions } = type;
return (
return isMobile ? (
<Flex justify="space-between">
<Segmented
defaultValue={status}
options={[
{
value: TASK_STATUS.PENDING,
label: lang('Pending'),
},
{
value: TASK_STATUS.COMPLETED,
label: lang('Completed'),
},
{
value: TASK_STATUS.ALL,
label: lang('All'),
},
]}
onChange={onSwitchTab}
/>
<Actions onlyIcon={isMobile} />
</Flex>
) : (
<Tabs
activeKey={status}
onChange={(activeKey) => {
navigate(`/admin/workflow/tasks/${taskType}/${activeKey}`);
}}
onChange={onSwitchTab}
className={css`
&.ant-tabs-top > .ant-tabs-nav {
margin-bottom: 0;
@ -142,8 +189,8 @@ function StatusTabs() {
function useTaskTypeItems() {
const workflowPlugin = usePlugin(PluginWorkflowClient);
const { counts } = useContext(TasksCountsContext);
const types = workflowPlugin.taskTypes.getKeys();
const { counts } = useContext(TasksCountsContext);
return useMemo(
() =>
@ -173,26 +220,30 @@ function PopupContext(props: any) {
const { taskType, status = TASK_STATUS.PENDING, popupId } = useParams();
const { record } = usePopupRecordContext();
const navigate = useNavigate();
const mobilePage = useMobilePage();
const setVisible = useCallback(
(visible: boolean) => {
if (!visible) {
if (window.history.state.idx) {
navigate(-1);
} else {
navigate(
mobilePage ? `/page/workflow/tasks/${taskType}/${status}` : `/admin/workflow/tasks/${taskType}/${status}`,
);
}
}
},
[mobilePage, navigate, status, taskType],
);
if (!popupId) {
return null;
}
return (
<ActionContextProvider
visible={Boolean(popupId)}
setVisible={(visible) => {
if (!visible) {
if (window.history.state.idx) {
navigate(-1);
} else {
navigate(`/admin/workflow/tasks/${taskType}/${status}`);
}
}
}}
openMode="modal"
>
return record ? (
<ActionContextProvider visible={Boolean(popupId)} setVisible={setVisible} openMode="modal" openSize="large">
<CollectionRecordProvider record={record}>{props.children}</CollectionRecordProvider>
</ActionContextProvider>
);
) : null;
}
const PopupRecordContext = createContext<any>({ record: null, setRecord: (record) => {} });
@ -200,18 +251,229 @@ export function usePopupRecordContext() {
return useContext(PopupRecordContext);
}
function TaskPageContent() {
const navigate = useNavigate();
const apiClient = useAPIClient();
const { taskType, status = TASK_STATUS.PENDING, popupId } = useParams();
const mobilePage = useMobilePage();
const [currentRecord, setCurrentRecord] = useState<any>(null);
const { token } = theme.useToken();
const items = useTaskTypeItems();
const { title, collection, action = 'list', useActionParams, Item, Detail, getPopupRecord } = useCurrentTaskType();
const params = useActionParams(status);
// useEffect(() => {
// setTitle?.(`${lang('Workflow todos')}${title ? `: ${compile(title)}` : ''}`);
// }, [taskType, status, setTitle, title, compile]);
useEffect(() => {
if (!taskType) {
navigate(
mobilePage
? `/page/workflow/tasks/${items[0].key}/${status}`
: `/admin/workflow/tasks/${items[0].key}/${status}`,
{ replace: true },
);
}
}, [items, mobilePage, navigate, status, taskType]);
useEffect(() => {
if (popupId && !currentRecord) {
let load;
if (getPopupRecord) {
load = getPopupRecord(apiClient, { params: { filterByTk: popupId } });
} else {
load = apiClient.resource(collection).get({
...params,
filterByTk: popupId,
});
}
load
.then((res) => {
if (res.data?.data) {
setCurrentRecord(res.data.data);
}
})
.catch((err) => {
console.error(err);
});
}
}, [popupId, collection, currentRecord, apiClient, getPopupRecord, params]);
useEffect(() => {
if (!taskType) {
navigate(
mobilePage
? `/page/workflow/tasks/${items[0].key}/${status}`
: `/admin/workflow/tasks/${items[0].key}/${status}`,
{ replace: true },
);
}
}, [items, mobilePage, navigate, status, taskType]);
const typeKey = taskType ?? items[0].key;
const { isMobileLayout } = useMobileLayout();
const isMobile = mobilePage || isMobileLayout;
const contentClass = css`
height: 100%;
overflow: hidden;
padding: 0;
.nb-list {
height: 100%;
overflow: hidden;
.nb-list-container {
height: 100%;
overflow: hidden;
.ant-formily-layout {
height: 100%;
overflow: hidden;
.ant-list {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
.ant-spin-nested-loading {
flex-grow: 1;
overflow: hidden;
.ant-spin-container {
height: 100%;
overflow: auto;
padding: ${isMobile ? '0.5em' : `${token.paddingContentHorizontalLG}px`};
}
}
.itemCss:not(:last-child) {
border-bottom: none;
}
}
.ant-list-pagination {
margin-top: 0;
padding: ${isMobile
? '0.5em'
: `${token.paddingContentHorizontal}px ${token.paddingContentHorizontalLG}px`};
border-top: 1px solid ${token.colorBorderSecondary};
}
}
}
}
`;
return (
<PopupRecordContext.Provider
value={{
record: currentRecord,
setRecord: setCurrentRecord,
}}
>
<SchemaComponentContext.Provider value={{ designable: false }}>
<SchemaComponent
components={{
Layout,
PageHeader,
StatusTabs,
}}
schema={{
name: `${taskType}-${status}`,
type: 'void',
'x-decorator': 'List.Decorator',
'x-decorator-props': {
collection,
action,
params: {
pageSize: 20,
sort: ['-createdAt'],
...params,
},
},
properties: {
header: {
type: 'void',
'x-component': 'PageHeader',
'x-component-props': {
className: classnames(
'pageHeaderCss',
css`
.ant-page-header-content {
padding-top: 0;
}
`,
),
style: {
position: 'sticky',
background: token.colorBgContainer,
padding: isMobile
? '8px'
: `${token.paddingContentVertical}px ${token.paddingContentHorizontalLG}px 0 ${token.paddingContentHorizontalLG}px`,
borderBottom: isMobile ? `1px solid ${token.colorBorderSecondary}` : null,
},
title: isMobile ? null : title,
},
properties: {
tabs: {
type: 'void',
'x-component': 'StatusTabs',
},
},
},
content: {
type: 'void',
'x-component': 'Layout.Content',
'x-component-props': {
className: contentClass,
},
properties: {
list: {
type: 'array',
'x-component': 'List',
'x-component-props': {
locale: {
emptyText: `{{ t("No data yet", { ns: "${NAMESPACE}" }) }}`,
},
},
properties: {
item: {
type: 'object',
'x-decorator': 'List.Item',
'x-component': Item,
'x-read-pretty': true,
},
},
},
},
},
popup: {
type: 'void',
'x-decorator': PopupContext,
'x-component': Detail,
},
},
}}
/>
</SchemaComponentContext.Provider>
</PopupRecordContext.Provider>
);
}
export function WorkflowTasks() {
const compile = useCompile();
const { setTitle } = useDocumentTitle();
const navigate = useNavigate();
const { taskType, status = TASK_STATUS.PENDING, popupId } = useParams();
const { taskType, status = TASK_STATUS.PENDING } = useParams();
const { token } = useToken();
const [currentRecord, setCurrentRecord] = useState<any>(null);
const items = useTaskTypeItems();
const { title, collection, action = 'list', useActionParams, Item, Detail } = useCurrentTaskType();
const params = useActionParams(status);
const { title } = useCurrentTaskType();
useEffect(() => {
setTitle?.(`${lang('Workflow todos')}${title ? `: ${compile(title)}` : ''}`);
@ -223,19 +485,21 @@ export function WorkflowTasks() {
}
}, [items, navigate, status, taskType]);
useEffect(() => {
if (popupId && !currentRecord) {
setCurrentRecord({ id: popupId });
}
}, [popupId, currentRecord]);
const typeKey = taskType ?? items[0].key;
const { isMobileLayout } = useMobileLayout();
return (
<Layout className={layoutClass}>
<Layout.Sider theme="light" breakpoint="md" collapsedWidth="0" zeroWidthTriggerStyle={{ top: 24 }}>
<Menu mode="inline" selectedKeys={[typeKey]} items={items} style={{ height: '100%' }} />
</Layout.Sider>
{isMobileLayout ? (
<Layout.Header style={{ background: token.colorBgContainer, padding: 0, height: '3em', lineHeight: '3em' }}>
<Menu mode="horizontal" selectedKeys={[typeKey]} items={items} />
</Layout.Header>
) : (
<Layout.Sider theme="light" breakpoint="md" collapsedWidth="0" zeroWidthTriggerStyle={{ top: 24 }}>
<Menu mode="inline" selectedKeys={[typeKey]} items={items} style={{ height: '100%' }} />
</Layout.Sider>
)}
<Layout
className={css`
> div {
@ -254,95 +518,7 @@ export function WorkflowTasks() {
}
`}
>
<PopupRecordContext.Provider
value={{
record: currentRecord,
setRecord: setCurrentRecord,
}}
>
<SchemaComponentContext.Provider value={{ designable: false }}>
<SchemaComponent
components={{
Layout,
PageHeader,
StatusTabs,
}}
schema={{
name: `${taskType}-${status}`,
type: 'void',
'x-decorator': 'List.Decorator',
'x-decorator-props': {
collection,
action,
params: {
pageSize: 20,
sort: ['-createdAt'],
...params,
},
},
properties: {
header: {
type: 'void',
'x-component': 'PageHeader',
'x-component-props': {
className: classnames('pageHeaderCss'),
style: {
background: token.colorBgContainer,
padding: '12px 24px 0 24px',
},
title,
},
properties: {
tabs: {
type: 'void',
'x-component': 'StatusTabs',
},
},
},
content: {
type: 'void',
'x-component': 'Layout.Content',
'x-component-props': {
className: contentClass,
style: {
padding: `${token.paddingPageVertical}px ${token.paddingPageHorizontal}px`,
},
},
properties: {
list: {
type: 'array',
'x-component': 'List',
'x-component-props': {
className: css`
> .itemCss:not(:last-child) {
border-bottom: none;
}
`,
locale: {
emptyText: `{{ t("No data yet", { ns: "${NAMESPACE}" }) }}`,
},
},
properties: {
item: {
type: 'object',
'x-decorator': 'List.Item',
'x-component': Item,
'x-read-pretty': true,
},
},
},
},
},
popup: {
type: 'void',
'x-decorator': PopupContext,
'x-component': Detail,
},
},
}}
/>
</SchemaComponentContext.Provider>
</PopupRecordContext.Provider>
<TaskPageContent />
</Layout>
</Layout>
);
@ -439,3 +615,168 @@ export function TasksProvider(props: any) {
return isLoggedIn ? <TasksCountsProvider>{content}</TasksCountsProvider> : content;
}
export const tasksSchemaInitializerItem: SchemaInitializerItemType = {
name: 'workflow-tasks-center',
type: 'item',
useComponentProps() {
const { resource, refresh, schemaResource } = useMobileRoutes();
const items = useTaskTypeItems();
return {
isItem: true,
title: lang('Workflow Tasks'),
badge: 10,
async onClick(values) {
const res = await resource.list();
if (Array.isArray(res?.data?.data)) {
const findIndex = res?.data?.data.findIndex((route) => route?.options?.url === `/page/workflow/tasks`);
if (findIndex > -1) {
Toast.show({
icon: 'fail',
content: lang('The workflow tasks page has already been created.'),
});
return;
}
}
const { data } = await resource.create({
values: {
type: 'page',
title: lang('Workflow Tasks'),
icon: 'CheckCircleOutlined',
schemaUid: 'workflow/tasks',
options: {
url: `/page/workflow/tasks`,
schema: {
'x-component': 'MobileTabBarWorkflowTasksItem',
},
},
// children: [
// {
// type: 'page',
// title: lang('Workflow tasks'),
// icon: 'CheckCircleOutlined',
// schemaUid: 'workflow-tasks',
// options: {
// url: `/page/workflow/tasks`,
// itemSchema: {
// name: uid(),
// 'x-decorator': 'BlockItem',
// 'x-settings': `mobile:tab-bar:page`,
// 'x-component': 'MobileTabBarWorkflowTasksItem',
// 'x-toolbar-props': {
// showBorder: false,
// showBackground: true,
// },
// },
// },
// },
// ],
} as MobileRouteItem,
});
// const parentId = data.data.id;
refresh();
},
};
},
};
export const MobileTabBarWorkflowTasksItem = observer(
(props: any) => {
const navigate = useNavigate();
const location = useLocation();
const items = useTaskTypeItems();
const onClick = useCallback(() => {
navigate(`/page/workflow/tasks/${items[0].key}/${TASK_STATUS.PENDING}`);
}, [items, navigate]);
const { total } = useContext(TasksCountsContext);
const selected = props.url && location.pathname.startsWith(props.url);
return (
<MobileTabBarItem
{...{
...props,
onClick,
badge: total > 0 ? total : undefined,
selected,
}}
/>
);
},
{
displayName: 'MobileTabBarWorkflowTasksItem',
},
);
export function WorkflowTasksMobile() {
const items = useTaskTypeItems();
const { token } = useToken();
const navigate = useNavigate();
return (
<MobilePageProvider>
<MobilePageHeader>
<NavBar className="nb-workflow-tasks-back-action" onBack={() => navigate(-1)}>
{lang('Workflow tasks')}
</NavBar>
<Tabs
className={css({
padding: `0 ${token.paddingPageHorizontal}px`,
'.adm-tabs-header': {
borderBottomWidth: 0,
},
'.adm-tabs-tab': {
height: 49,
padding: '10px 0 10px',
},
'> .ant-tabs-nav': {
marginBottom: 0,
'&::before': {
borderBottom: 'none',
},
},
'.ant-tabs-tab+.ant-tabs-tab': {
marginLeft: '2em',
},
})}
items={items}
/>
</MobilePageHeader>
<MobilePageContentContainer
className={css`
padding: 0 !important;
> div {
height: 100%;
overflow: hidden;
> .ant-formily-layout {
height: 100%;
overflow: hidden;
> div {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
}
}
.ant-nb-list {
.itemCss:not(:last-child) {
padding-bottom: 0;
margin-bottom: 0.5em;
}
.itemCss:not(:first-child) {
padding-top: 0;
margin-top: 0.5em;
}
}
`}
>
<TaskPageContent />
</MobilePageContentContainer>
</MobilePageProvider>
);
}

View File

@ -7,13 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { PagePopups, Plugin, useCompile } from '@nocobase/client';
import { PagePopups, Plugin, useCompile, lazy } from '@nocobase/client';
import { Registry } from '@nocobase/utils/client';
import MobileManager from '@nocobase/plugin-mobile/client';
// import { ExecutionPage } from './ExecutionPage';
// import { WorkflowPage } from './WorkflowPage';
// import { WorkflowPane } from './WorkflowPane';
import { lazy } from '@nocobase/client';
const { ExecutionPage } = lazy(() => import('./ExecutionPage'), 'ExecutionPage');
const { WorkflowPage } = lazy(() => import('./WorkflowPage'), 'WorkflowPage');
const { WorkflowPane } = lazy(() => import('./WorkflowPane'), 'WorkflowPane');
@ -33,8 +33,16 @@ import CollectionTrigger from './triggers/collection';
import ScheduleTrigger from './triggers/schedule';
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
import { VariableOption } from './variable';
import { TasksProvider, TaskTypeOptions, WorkflowTasks } from './WorkflowTasks';
import {
MobileTabBarWorkflowTasksItem,
TasksProvider,
tasksSchemaInitializerItem,
TaskTypeOptions,
WorkflowTasks,
WorkflowTasksMobile,
} from './WorkflowTasks';
import { WorkflowCollectionsProvider } from './WorkflowCollectionsProvider';
import { observer } from '@formily/react';
const workflowConfigSettings = {
Component: BindWorkflowConfig,
@ -111,6 +119,22 @@ export default class PluginWorkflowClient extends Plugin {
async load() {
this.app.addProvider(WorkflowCollectionsProvider);
this.app.addProvider(TasksProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {
icon: 'PartitionOutlined',
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
Component: WorkflowPane,
aclSnippet: 'pm.workflow.workflows',
});
this.app.schemaSettingsManager.addItem('actionSettings:submit', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:createSubmit', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:updateSubmit', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:saveRecord', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:updateRecord', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:delete', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:bulkEditSubmit', 'workflowConfig', workflowConfigSettings);
this.router.add('admin.workflow.workflows.id', {
path: getWorkflowDetailPath(':id'),
@ -127,22 +151,18 @@ export default class PluginWorkflowClient extends Plugin {
Component: WorkflowTasks,
});
this.app.pluginSettingsManager.add(NAMESPACE, {
icon: 'PartitionOutlined',
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
Component: WorkflowPane,
aclSnippet: 'pm.workflow.workflows',
});
this.app.use(TasksProvider);
this.app.schemaSettingsManager.addItem('actionSettings:submit', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:createSubmit', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:updateSubmit', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:saveRecord', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:updateRecord', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:delete', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:bulkEditSubmit', 'workflowConfig', workflowConfigSettings);
const mobileManager = this.pm.get(MobileManager);
this.app.schemaInitializerManager.addItem('mobile:tab-bar', 'workflow-tasks', tasksSchemaInitializerItem);
this.app.addComponents({ MobileTabBarWorkflowTasksItem });
if (mobileManager.mobileRouter) {
mobileManager.mobileRouter.add('mobile.page.workflow', {
path: '/page/workflow',
});
mobileManager.mobileRouter.add('mobile.page.workflow.tasks', {
path: '/page/workflow/tasks/:taskType/:status/:popupId?',
Component: observer(WorkflowTasksMobile, { displayName: 'WorkflowTasksMobile' }),
});
}
this.registerInstructionGroup('control', { key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` });
this.registerInstructionGroup('calculation', {