/** * 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, useNavigate, useParams } from 'react-router-dom'; import { ActionContextProvider, CollectionRecordProvider, css, PinnedPluginListProvider, SchemaComponent, SchemaComponentContext, SchemaComponentOptions, useAPIClient, 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 contentClass = css` 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[]; alwaysShow?: boolean; } type Stats = Record; const TasksCountsContext = createContext<{ reload: () => void; counts: Stats; 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); const { counts } = useContext(TasksCountsContext); const types = workflowPlugin.taskTypes.getKeys(); return useMemo( () => Array.from(types) .filter((key: string) => workflowPlugin.taskTypes.get(key)?.alwaysShow || Boolean(counts[key]?.all)) .map((key: string) => { return { key, label: , }; }), [counts, types, 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 { taskType, status = TASK_STATUS.PENDING, popupId } = useParams(); const { record } = usePopupRecordContext(); const navigate = useNavigate(); if (!popupId) { return null; } return ( { if (!visible) { if (window.history.state.idx) { navigate(-1); } else { navigate(`/admin/workflow/tasks/${taskType}/${status}`); } } }} openMode="modal" > {props.children} ); } const PopupRecordContext = createContext({ record: null, setRecord: (record) => {} }); export function usePopupRecordContext() { return useContext(PopupRecordContext); } export function WorkflowTasks() { const compile = useCompile(); const { setTitle } = useDocumentTitle(); const navigate = useNavigate(); const { taskType, status = TASK_STATUS.PENDING, popupId } = 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); 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]); useEffect(() => { if (popupId && !currentRecord) { setCurrentRecord({ id: popupId }); } }, [popupId, currentRecord]); 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 { reload, total } = useContext(TasksCountsContext); const items = useTaskTypeItems(); return items.length ? ( ) : null; } function transform(records) { return records.reduce((result, record) => { result[record.type] = record.stats; return result; }, {}); } function TasksCountsProvider(props: any) { const app = useApp(); const [counts, setCounts] = useState({}); const onTaskUpdate = useCallback(({ detail }: CustomEvent) => { setCounts((prev) => ({ ...prev, ...transform([detail]), })); }, []); const { runAsync } = useRequest( { resource: 'userWorkflowTasks', action: 'listMine', }, { 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((result, item) => result + (item.pending || 0), 0) || 0; return {props.children}; } export function TasksProvider(props: any) { const isLoggedIn = useIsLoggedIn(); const content = ( {props.children} ); return isLoggedIn ? {content} : content; }