/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * 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 { PageHeader } from '@ant-design/pro-layout'; import { Badge, Button, Layout, Menu, Tabs, Tooltip } from 'antd'; import classnames from 'classnames'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Link, Outlet, useNavigate, useParams } from 'react-router-dom'; import { ActionContextProvider, CollectionRecordProvider, css, PinnedPluginListProvider, SchemaComponent, SchemaComponentContext, SchemaComponentOptions, useApp, useCompile, useDocumentTitle, useIsLoggedIn, usePlugin, useRequest, useToken, } from '@nocobase/client'; import PluginWorkflowClient from '.'; import { lang, NAMESPACE } from './locale'; const layoutClass = css` height: 100%; overflow: hidden; `; const sideClass = css` overflow: auto; position: sticky; top: 0; bottom: 0; height: 100%; .ant-layout-sider-children { width: 200px; height: 100%; } `; const contentClass = css` padding: 24px; min-height: 280px; overflow: auto; `; export interface TaskTypeOptions { title: string; collection: string; action: string; useActionParams: Function; Actions?: React.ComponentType; Item: React.ComponentType; Detail: React.ComponentType; // children?: TaskTypeOptions[]; } const TasksCountsContext = createContext<{ reload: () => void; counts: Record; total: number }>({ reload() {}, counts: {}, total: 0, }); function MenuLink({ type }: any) { const workflowPlugin = usePlugin(PluginWorkflowClient); const compile = useCompile(); const { title } = workflowPlugin.taskTypes.get(type); const { counts } = useContext(TasksCountsContext); const typeTitle = compile(title); return ( span:first-child { overflow: hidden; text-overflow: ellipsis; } `} > {typeTitle} ); } export const TASK_STATUS = { ALL: 'all', PENDING: 'pending', COMPLETED: 'completed', }; function StatusTabs() { const navigate = useNavigate(); const { taskType, status = TASK_STATUS.PENDING } = useParams(); const type = useCurrentTaskType(); const { Actions } = type; return ( { navigate(`/admin/workflow/tasks/${taskType}/${activeKey}`); }} className={css` &.ant-tabs-top > .ant-tabs-nav { margin-bottom: 0; } `} items={[ { key: TASK_STATUS.PENDING, label: lang('Pending'), }, { key: TASK_STATUS.COMPLETED, label: lang('Completed'), }, { key: TASK_STATUS.ALL, label: lang('All'), }, ]} tabBarExtraContent={ Actions ? { right: , } : {} } /> ); } function useTaskTypeItems() { const workflowPlugin = usePlugin(PluginWorkflowClient); return useMemo( () => Array.from(workflowPlugin.taskTypes.getKeys()).map((key: string) => { return { key, label: , }; }), [workflowPlugin.taskTypes], ); } function useCurrentTaskType() { const workflowPlugin = usePlugin(PluginWorkflowClient); const { taskType } = useParams(); const items = useTaskTypeItems(); return useMemo( () => workflowPlugin.taskTypes.get(taskType ?? items[0]?.key) ?? {}, [items, taskType, workflowPlugin.taskTypes], ); } function PopupContext(props: any) { const { popupId } = useParams(); const navigate = useNavigate(); return ( { if (!visible) { navigate(-1); } }} openMode="modal" > {props.children} ); } export function WorkflowTasks() { const compile = useCompile(); const { setTitle } = useDocumentTitle(); const navigate = useNavigate(); const { taskType, status = TASK_STATUS.PENDING, popupId } = useParams(); const { token: { colorBgContainer }, } = useToken(); const items = useTaskTypeItems(); const { title, collection, action = 'list', useActionParams, Item, Detail } = useCurrentTaskType(); const params = useActionParams(status); useEffect(() => { setTitle?.(`${lang('Workflow todos')}${title ? `: ${compile(title)}` : ''}`); }, [taskType, status, setTitle, title, compile]); useEffect(() => { if (!taskType) { navigate(`/admin/workflow/tasks/${items[0].key}/${status}`, { replace: true }); } }, [items, navigate, status, taskType]); const typeKey = taskType ?? items[0].key; return ( div { height: 100%; overflow: hidden; > .ant-formily-layout { height: 100%; > div { display: flex; flex-direction: column; height: 100%; } } } `} > .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, }, }, }} /> ); } function WorkflowTasksLink() { const workflowPlugin = usePlugin(PluginWorkflowClient); const { reload, total } = useContext(TasksCountsContext); const types = Array.from(workflowPlugin.taskTypes.getKeys()); return types.length ? ( ) : null; } function transform(detail) { return detail.reduce((result, stats) => { result[stats.type] = stats.count; return result; }, {}); } function TasksCountsProvider(props: any) { const app = useApp(); const [counts, setCounts] = useState>({}); const onTaskUpdate = useCallback(({ detail = [] }: CustomEvent) => { setCounts(transform(detail)); }, []); const { runAsync } = useRequest( { resource: 'workflowTasks', action: 'countMine', }, { manual: true, }, ); const reload = useCallback(() => { runAsync() .then((res) => { setCounts(transform(res['data'])); }) .catch((err) => { console.error(err); }); }, [runAsync]); useEffect(() => { reload(); }, [reload]); useEffect(() => { app.eventBus.addEventListener('ws:message:workflow:tasks:updated', onTaskUpdate); return () => { app.eventBus.removeEventListener('ws:message:workflow:tasks:updated', onTaskUpdate); }; }, [app.eventBus, onTaskUpdate]); const total = Object.values(counts).reduce((a, b) => a + b, 0) || 0; return {props.children}; } export function TasksProvider(props: any) { const isLoggedIn = useIsLoggedIn(); const content = ( {props.children} ); return isLoggedIn ? {content} : content; }