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', {