fix(plugin-workflow): optimize mobile style (#7040)

* fix(plugin-workflow): optimize mobile style

* fix(plugin-workflow): fix mobile style

* feat(plugin-workflow): add tasks to mobile menu initializer

* refactor(plugin-workflow): adjust tasks center api

* fix(ActionDrawer): apply zIndex style to ActionDrawer component

* fix(plugin-workflow-manual): fix manual style under mobile

* fix(plugin-workflow): fix styles

* fix(plugin-workflow): fix mobile layout check

* fix(plugin-workflow): adjust pagination footer style

* fix(plugin-workflow): fix icon only props

* fix(plugin-workflow-manual): fix todo item title

* revert(plugin-field-sort): revert mistaken commit

---------

Co-authored-by: Zeke Zhang <958414905@qq.com>
This commit is contained in:
Junyi 2025-06-24 22:18:02 +08:00 committed by GitHub
parent e527e4d6ba
commit a9bd6f7844
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 553 additions and 167 deletions

View File

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

View File

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

View File

@ -53,7 +53,11 @@ const useLocalVariables = (props?: Props) => {
dataSource: parentPopupDataSource, dataSource: parentPopupDataSource,
defaultValue: defaultValueOfParentPopupRecord, defaultValue: defaultValueOfParentPopupRecord,
} = useParentPopupVariableContext(); } = useParentPopupVariableContext();
const { urlSearchParamsCtx, shouldDisplay: shouldDisplayURLSearchParams, defaultValue: defaultValueOfURLSearchParams } = useURLSearchParamsVariable(); const {
urlSearchParamsCtx,
shouldDisplay: shouldDisplayURLSearchParams,
defaultValue: defaultValueOfURLSearchParams,
} = useURLSearchParamsVariable();
const { datetimeCtx } = useDatetimeVariableContext(); const { datetimeCtx } = useDatetimeVariableContext();
const { currentFormCtx } = useCurrentFormContext({ form: props?.currentForm }); const { currentFormCtx } = useCurrentFormContext({ form: props?.currentForm });
const { name: currentCollectionName } = useCollection_deprecated(); const { name: currentCollectionName } = useCollection_deprecated();

View File

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

View File

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

View File

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

View File

@ -40,6 +40,7 @@
"@nocobase/logger": "1.x", "@nocobase/logger": "1.x",
"@nocobase/plugin-data-source-main": "1.x", "@nocobase/plugin-data-source-main": "1.x",
"@nocobase/plugin-error-handler": "1.x", "@nocobase/plugin-error-handler": "1.x",
"@nocobase/plugin-mobile": "1.x",
"@nocobase/plugin-users": "1.x", "@nocobase/plugin-users": "1.x",
"@nocobase/resourcer": "1.x", "@nocobase/resourcer": "1.x",
"@nocobase/server": "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. * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 { 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 classnames from 'classnames';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; 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 { import {
ActionContextProvider, ActionContextProvider,
@ -26,11 +28,25 @@ import {
useCompile, useCompile,
useDocumentTitle, useDocumentTitle,
useIsLoggedIn, useIsLoggedIn,
useMobileLayout,
usePlugin, usePlugin,
useRequest, useRequest,
useToken, useToken,
SchemaInitializerItemType,
APIClient,
} from '@nocobase/client'; } from '@nocobase/client';
import {
MobilePageContentContainer,
MobilePageHeader,
MobilePageNavigationBar,
MobilePageProvider,
MobileRouteItem,
MobileTabBarItem,
useMobilePage,
useMobileRoutes,
} from '@nocobase/plugin-mobile/client';
import PluginWorkflowClient from '.'; import PluginWorkflowClient from '.';
import { lang, NAMESPACE } from './locale'; import { lang, NAMESPACE } from './locale';
@ -39,11 +55,6 @@ const layoutClass = css`
overflow: hidden; overflow: hidden;
`; `;
const contentClass = css`
min-height: 280px;
overflow: auto;
`;
export interface TaskTypeOptions { export interface TaskTypeOptions {
title: string; title: string;
collection: string; collection: string;
@ -52,6 +63,7 @@ export interface TaskTypeOptions {
Actions?: React.ComponentType; Actions?: React.ComponentType;
Item: React.ComponentType; Item: React.ComponentType;
Detail: React.ComponentType; Detail: React.ComponentType;
getPopupRecord?: (apiClient: APIClient, { params }: { params: any }) => Promise<any>;
// children?: TaskTypeOptions[]; // children?: TaskTypeOptions[];
alwaysShow?: boolean; alwaysShow?: boolean;
} }
@ -70,12 +82,18 @@ function MenuLink({ type }: any) {
const { title } = workflowPlugin.taskTypes.get(type); const { title } = workflowPlugin.taskTypes.get(type);
const { counts } = useContext(TasksCountsContext); const { counts } = useContext(TasksCountsContext);
const typeTitle = compile(title); const typeTitle = compile(title);
const mobilePage = useMobilePage();
return ( return (
<Link <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` className={css`
display: flex; display: flex;
gap: 0.5em;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
@ -103,13 +121,42 @@ function StatusTabs() {
const navigate = useNavigate(); const navigate = useNavigate();
const { taskType, status = TASK_STATUS.PENDING } = useParams(); const { taskType, status = TASK_STATUS.PENDING } = useParams();
const type = useCurrentTaskType(); 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; 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 <Tabs
activeKey={status} activeKey={status}
onChange={(activeKey) => { onChange={onSwitchTab}
navigate(`/admin/workflow/tasks/${taskType}/${activeKey}`);
}}
className={css` className={css`
&.ant-tabs-top > .ant-tabs-nav { &.ant-tabs-top > .ant-tabs-nav {
margin-bottom: 0; margin-bottom: 0;
@ -142,8 +189,8 @@ function StatusTabs() {
function useTaskTypeItems() { function useTaskTypeItems() {
const workflowPlugin = usePlugin(PluginWorkflowClient); const workflowPlugin = usePlugin(PluginWorkflowClient);
const { counts } = useContext(TasksCountsContext);
const types = workflowPlugin.taskTypes.getKeys(); const types = workflowPlugin.taskTypes.getKeys();
const { counts } = useContext(TasksCountsContext);
return useMemo( return useMemo(
() => () =>
@ -173,26 +220,30 @@ function PopupContext(props: any) {
const { taskType, status = TASK_STATUS.PENDING, popupId } = useParams(); const { taskType, status = TASK_STATUS.PENDING, popupId } = useParams();
const { record } = usePopupRecordContext(); const { record } = usePopupRecordContext();
const navigate = useNavigate(); 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) { if (!popupId) {
return null; return null;
} }
return (
<ActionContextProvider return record ? (
visible={Boolean(popupId)} <ActionContextProvider visible={Boolean(popupId)} setVisible={setVisible} openMode="modal" openSize="large">
setVisible={(visible) => {
if (!visible) {
if (window.history.state.idx) {
navigate(-1);
} else {
navigate(`/admin/workflow/tasks/${taskType}/${status}`);
}
}
}}
openMode="modal"
>
<CollectionRecordProvider record={record}>{props.children}</CollectionRecordProvider> <CollectionRecordProvider record={record}>{props.children}</CollectionRecordProvider>
</ActionContextProvider> </ActionContextProvider>
); ) : null;
} }
const PopupRecordContext = createContext<any>({ record: null, setRecord: (record) => {} }); const PopupRecordContext = createContext<any>({ record: null, setRecord: (record) => {} });
@ -200,18 +251,229 @@ export function usePopupRecordContext() {
return useContext(PopupRecordContext); 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: { ...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 (
<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() { export function WorkflowTasks() {
const compile = useCompile(); const compile = useCompile();
const { setTitle } = useDocumentTitle(); const { setTitle } = useDocumentTitle();
const navigate = useNavigate(); const navigate = useNavigate();
const { taskType, status = TASK_STATUS.PENDING, popupId } = useParams(); const { taskType, status = TASK_STATUS.PENDING } = useParams();
const { token } = useToken(); const { token } = useToken();
const [currentRecord, setCurrentRecord] = useState<any>(null);
const items = useTaskTypeItems(); const items = useTaskTypeItems();
const { title, collection, action = 'list', useActionParams, Item, Detail } = useCurrentTaskType(); const { title } = useCurrentTaskType();
const params = useActionParams(status);
useEffect(() => { useEffect(() => {
setTitle?.(`${lang('Workflow todos')}${title ? `: ${compile(title)}` : ''}`); setTitle?.(`${lang('Workflow todos')}${title ? `: ${compile(title)}` : ''}`);
@ -223,19 +485,21 @@ export function WorkflowTasks() {
} }
}, [items, navigate, status, taskType]); }, [items, navigate, status, taskType]);
useEffect(() => {
if (popupId && !currentRecord) {
setCurrentRecord({ id: popupId });
}
}, [popupId, currentRecord]);
const typeKey = taskType ?? items[0].key; const typeKey = taskType ?? items[0].key;
const { isMobileLayout } = useMobileLayout();
return ( return (
<Layout className={layoutClass}> <Layout className={layoutClass}>
<Layout.Sider theme="light" breakpoint="md" collapsedWidth="0" zeroWidthTriggerStyle={{ top: 24 }}> {isMobileLayout ? (
<Menu mode="inline" selectedKeys={[typeKey]} items={items} style={{ height: '100%' }} /> <Layout.Header style={{ background: token.colorBgContainer, padding: 0, height: '3em', lineHeight: '3em' }}>
</Layout.Sider> <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 <Layout
className={css` className={css`
> div { > div {
@ -254,95 +518,7 @@ export function WorkflowTasks() {
} }
`} `}
> >
<PopupRecordContext.Provider <TaskPageContent />
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>
</Layout> </Layout>
</Layout> </Layout>
); );
@ -439,3 +615,168 @@ export function TasksProvider(props: any) {
return isLoggedIn ? <TasksCountsProvider>{content}</TasksCountsProvider> : content; 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. * 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 { Registry } from '@nocobase/utils/client';
import MobileManager from '@nocobase/plugin-mobile/client';
// import { ExecutionPage } from './ExecutionPage'; // import { ExecutionPage } from './ExecutionPage';
// import { WorkflowPage } from './WorkflowPage'; // import { WorkflowPage } from './WorkflowPage';
// import { WorkflowPane } from './WorkflowPane'; // import { WorkflowPane } from './WorkflowPane';
import { lazy } from '@nocobase/client';
const { ExecutionPage } = lazy(() => import('./ExecutionPage'), 'ExecutionPage'); const { ExecutionPage } = lazy(() => import('./ExecutionPage'), 'ExecutionPage');
const { WorkflowPage } = lazy(() => import('./WorkflowPage'), 'WorkflowPage'); const { WorkflowPage } = lazy(() => import('./WorkflowPage'), 'WorkflowPage');
const { WorkflowPane } = lazy(() => import('./WorkflowPane'), 'WorkflowPane'); const { WorkflowPane } = lazy(() => import('./WorkflowPane'), 'WorkflowPane');
@ -33,8 +33,16 @@ import CollectionTrigger from './triggers/collection';
import ScheduleTrigger from './triggers/schedule'; import ScheduleTrigger from './triggers/schedule';
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils'; import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
import { VariableOption } from './variable'; import { VariableOption } from './variable';
import { TasksProvider, TaskTypeOptions, WorkflowTasks } from './WorkflowTasks'; import {
MobileTabBarWorkflowTasksItem,
TasksProvider,
tasksSchemaInitializerItem,
TaskTypeOptions,
WorkflowTasks,
WorkflowTasksMobile,
} from './WorkflowTasks';
import { WorkflowCollectionsProvider } from './WorkflowCollectionsProvider'; import { WorkflowCollectionsProvider } from './WorkflowCollectionsProvider';
import { observer } from '@formily/react';
const workflowConfigSettings = { const workflowConfigSettings = {
Component: BindWorkflowConfig, Component: BindWorkflowConfig,
@ -111,6 +119,22 @@ export default class PluginWorkflowClient extends Plugin {
async load() { async load() {
this.app.addProvider(WorkflowCollectionsProvider); 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', { this.router.add('admin.workflow.workflows.id', {
path: getWorkflowDetailPath(':id'), path: getWorkflowDetailPath(':id'),
@ -127,22 +151,18 @@ export default class PluginWorkflowClient extends Plugin {
Component: WorkflowTasks, Component: WorkflowTasks,
}); });
this.app.pluginSettingsManager.add(NAMESPACE, { const mobileManager = this.pm.get(MobileManager);
icon: 'PartitionOutlined', this.app.schemaInitializerManager.addItem('mobile:tab-bar', 'workflow-tasks', tasksSchemaInitializerItem);
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`, this.app.addComponents({ MobileTabBarWorkflowTasksItem });
Component: WorkflowPane, if (mobileManager.mobileRouter) {
aclSnippet: 'pm.workflow.workflows', mobileManager.mobileRouter.add('mobile.page.workflow', {
}); path: '/page/workflow',
});
this.app.use(TasksProvider); mobileManager.mobileRouter.add('mobile.page.workflow.tasks', {
path: '/page/workflow/tasks/:taskType/:status/:popupId?',
this.app.schemaSettingsManager.addItem('actionSettings:submit', 'workflowConfig', workflowConfigSettings); Component: observer(WorkflowTasksMobile, { displayName: 'WorkflowTasksMobile' }),
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.registerInstructionGroup('control', { key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` }); this.registerInstructionGroup('control', { key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` });
this.registerInstructionGroup('calculation', { this.registerInstructionGroup('calculation', {