diff --git a/packages/core/client/src/schema-component/antd/list/List.style.ts b/packages/core/client/src/schema-component/antd/list/List.style.ts
index d3ad6c3e6a..9e9a083057 100644
--- a/packages/core/client/src/schema-component/antd/list/List.style.ts
+++ b/packages/core/client/src/schema-component/antd/list/List.style.ts
@@ -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}`,
},
},
diff --git a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx
index 81b015b5bc..fd701c9619 100644
--- a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx
+++ b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx
@@ -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}
);
}
diff --git a/packages/core/client/src/variables/hooks/useLocalVariables.tsx b/packages/core/client/src/variables/hooks/useLocalVariables.tsx
index d87ff68b37..2590e7ab87 100644
--- a/packages/core/client/src/variables/hooks/useLocalVariables.tsx
+++ b/packages/core/client/src/variables/hooks/useLocalVariables.tsx
@@ -53,7 +53,11 @@ const useLocalVariables = (props?: Props) => {
dataSource: parentPopupDataSource,
defaultValue: defaultValueOfParentPopupRecord,
} = useParentPopupVariableContext();
- const { urlSearchParamsCtx, shouldDisplay: shouldDisplayURLSearchParams, defaultValue: defaultValueOfURLSearchParams } = useURLSearchParamsVariable();
+ const {
+ urlSearchParamsCtx,
+ shouldDisplay: shouldDisplayURLSearchParams,
+ defaultValue: defaultValueOfURLSearchParams,
+ } = useURLSearchParamsVariable();
const { datetimeCtx } = useDatetimeVariableContext();
const { currentFormCtx } = useCurrentFormContext({ form: props?.currentForm });
const { name: currentCollectionName } = useCollection_deprecated();
diff --git a/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateAction.tsx b/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateAction.tsx
index 079eea5467..37917853a0 100644
--- a/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateAction.tsx
+++ b/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateAction.tsx
@@ -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();
diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/MobilePageContentContainer.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/MobilePageContentContainer.tsx
index 1285cf538a..2373039bac 100644
--- a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/MobilePageContentContainer.tsx
+++ b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/MobilePageContentContainer.tsx
@@ -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 }> = ({ children, hideTabBar }) => {
+export const MobilePageContentContainer: FC<{ hideTabBar?: boolean; className?: string }> = ({
+ children,
+ hideTabBar,
+ className,
+}) => {
const [mobileTabBarHeight, setMobileTabBarHeight] = React.useState(0);
const [mobilePageHeader, setMobilePageHeader] = React.useState(0);
const { token } = useToken();
@@ -31,7 +36,7 @@ export const MobilePageContentContainer: FC<{ hideTabBar?: boolean }> = ({ child
<>
{mobilePageHeader ?
: null}
@@ -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 ? (
- );
+ ) : 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 (
Promise;
// 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 (
{
+ 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 ? (
+
+
+
+
+ ) : (
{
- 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 (
- {
- if (!visible) {
- if (window.history.state.idx) {
- navigate(-1);
- } else {
- navigate(`/admin/workflow/tasks/${taskType}/${status}`);
- }
- }
- }}
- openMode="modal"
- >
+
+ return record ? (
+
{props.children}
- );
+ ) : null;
}
const PopupRecordContext = createContext({ 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(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: { ...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]);
+
+ 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 (
+
+
+
+
+
+ );
+}
+
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(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 (
-
-
-
+ {isMobileLayout ? (
+
+
+
+ ) : (
+
+
+
+ )}
div {
@@ -254,95 +518,7 @@ export function WorkflowTasks() {
}
`}
>
-
-
- .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,
- },
- },
- }}
- />
-
-
+
);
@@ -439,3 +615,168 @@ export function TasksProvider(props: any) {
return isLoggedIn ? {content} : 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 (
+ 0 ? total : undefined,
+ selected,
+ }}
+ />
+ );
+ },
+ {
+ displayName: 'MobileTabBarWorkflowTasksItem',
+ },
+);
+
+export function WorkflowTasksMobile() {
+ const items = useTaskTypeItems();
+ const { token } = useToken();
+ const navigate = useNavigate();
+
+ return (
+
+
+ navigate(-1)}>
+ {lang('Workflow tasks')}
+
+ .ant-tabs-nav': {
+ marginBottom: 0,
+ '&::before': {
+ borderBottom: 'none',
+ },
+ },
+
+ '.ant-tabs-tab+.ant-tabs-tab': {
+ marginLeft: '2em',
+ },
+ })}
+ items={items}
+ />
+
+ 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;
+ }
+ }
+ `}
+ >
+
+
+
+ );
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx
index 42c2f61358..fbda6950f9 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx
@@ -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', {