diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index 35b496db1f..1c760cd4ba 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -460,8 +460,16 @@ exports.initEnv = function initEnv() { process.env.SOCKET_PATH = generateGatewayPath(); fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true }); fs.mkdirpSync(process.env.PM2_HOME, { force: true, recursive: true }); - const pkgDir = resolve(process.cwd(), 'storage/plugins', '@nocobase/plugin-multi-app-manager'); - fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true }); + const pkgs = [ + '@nocobase/plugin-multi-app-manager', + '@nocobase/plugin-departments', + '@nocobase/plugin-field-attachment-url', + '@nocobase/plugin-workflow-response-message', + ]; + for (const pkg of pkgs) { + const pkgDir = resolve(process.cwd(), 'storage/plugins', pkg); + fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true }); + } }; exports.generatePlugins = function () { diff --git a/packages/plugins/@nocobase/plugin-departments/.npmignore b/packages/plugins/@nocobase/plugin-departments/.npmignore new file mode 100644 index 0000000000..65f5e8779f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/.npmignore @@ -0,0 +1,2 @@ +/node_modules +/src diff --git a/packages/plugins/@nocobase/plugin-departments/README.md b/packages/plugins/@nocobase/plugin-departments/README.md new file mode 100644 index 0000000000..cb4fb63505 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/README.md @@ -0,0 +1 @@ +# @nocobase/plugin-department diff --git a/packages/plugins/@nocobase/plugin-departments/client.d.ts b/packages/plugins/@nocobase/plugin-departments/client.d.ts new file mode 100644 index 0000000000..6c459cbac4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/client.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client'; +export { default } from './dist/client'; diff --git a/packages/plugins/@nocobase/plugin-departments/client.js b/packages/plugins/@nocobase/plugin-departments/client.js new file mode 100644 index 0000000000..b6e3be70e6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/client.js @@ -0,0 +1 @@ +module.exports = require('./dist/client/index.js'); diff --git a/packages/plugins/@nocobase/plugin-departments/package.json b/packages/plugins/@nocobase/plugin-departments/package.json new file mode 100644 index 0000000000..ef7871ed9b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/package.json @@ -0,0 +1,22 @@ +{ + "name": "@nocobase/plugin-departments", + "displayName": "Departments", + "displayName.zh-CN": "部门", + "description": "Organize users by departments, set hierarchical relationships, link roles to control permissions, and use departments as variables in workflows and expressions.", + "description.zh-CN": "以部门来组织用户,设定上下级关系,绑定角色控制权限,并支持作为变量用于工作流和表达式。", + "version": "1.6.19", + "main": "dist/server/index.js", + "devDependencies": { + "@nocobase/plugin-user-data-sync": "1.x" + }, + "peerDependencies": { + "@nocobase/actions": "1.x", + "@nocobase/client": "1.x", + "@nocobase/server": "1.x", + "@nocobase/test": "1.x" + }, + "keywords": [ + "Users & permissions" + ], + "gitHead": "080fc78c1a744d47e010b3bbe5840446775800e4" +} diff --git a/packages/plugins/@nocobase/plugin-departments/server.d.ts b/packages/plugins/@nocobase/plugin-departments/server.d.ts new file mode 100644 index 0000000000..c41081ddc6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/server.d.ts @@ -0,0 +1,2 @@ +export * from './dist/server'; +export { default } from './dist/server'; diff --git a/packages/plugins/@nocobase/plugin-departments/server.js b/packages/plugins/@nocobase/plugin-departments/server.js new file mode 100644 index 0000000000..972842039a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/server.js @@ -0,0 +1 @@ +module.exports = require('./dist/server/index.js'); diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/ResourcesProvider.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/ResourcesProvider.tsx new file mode 100644 index 0000000000..2ad0daac83 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/ResourcesProvider.tsx @@ -0,0 +1,110 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { CollectionProvider_deprecated, ResourceActionContext, TableBlockContext, useRequest } from '@nocobase/client'; +import React, { useContext, useEffect, useMemo } from 'react'; +import { departmentCollection } from './collections/departments'; +import { userCollection } from './collections/users'; +import { FormContext } from '@formily/react'; +import { createForm } from '@formily/core'; + +export const ResourcesContext = React.createContext<{ + user: any; + setUser?: (user: any) => void; + department: any; // department name + setDepartment?: (department: any) => void; + departmentsResource?: any; + usersResource?: any; +}>({ + user: {}, + department: {}, +}); + +export const ResourcesProvider: React.FC = (props) => { + const [user, setUser] = React.useState(null); + const [department, setDepartment] = React.useState(null); + + const userService = useRequest({ + resource: 'users', + action: 'list', + params: { + appends: ['departments', 'departments.parent(recursively=true)'], + filter: department + ? { + 'departments.id': department.id, + } + : {}, + pageSize: 20, + }, + }); + + useEffect(() => { + userService.run(); + }, [department]); + + const departmentRequest = { + resource: 'departments', + action: 'list', + params: { + paginate: false, + filter: { + parentId: null, + }, + }, + }; + const departmentService = useRequest(departmentRequest); + + return ( + + {props.children} + + ); +}; + +export const DepartmentsListProvider: React.FC = (props) => { + const { departmentsResource } = useContext(ResourcesContext); + const { service } = departmentsResource || {}; + return ( + + {props.children} + + ); +}; + +export const UsersListProvider: React.FC = (props) => { + const { usersResource } = useContext(ResourcesContext); + const { service } = usersResource || {}; + const form = useMemo(() => createForm(), []); + const field = form.createField({ name: 'table' }); + return ( + + + {props.children} + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/collections/departments.ts b/packages/plugins/@nocobase/plugin-departments/src/client/collections/departments.ts new file mode 100644 index 0000000000..38105c0b2b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/collections/departments.ts @@ -0,0 +1,115 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +export const departmentCollection = { + name: 'departments', + fields: [ + { + type: 'bigInt', + name: 'id', + primaryKey: true, + autoIncrement: true, + interface: 'id', + uiSchema: { + type: 'id', + title: '{{t("ID")}}', + }, + }, + { + name: 'title', + type: 'string', + interface: 'input', + uiSchema: { + type: 'string', + title: '{{t("Department name")}}', + 'x-component': 'Input', + required: true, + }, + }, + { + name: 'parent', + type: 'belongsTo', + interface: 'm2o', + collectionName: 'departments', + foreignKey: 'parentId', + target: 'departments', + targetKey: 'id', + treeParent: true, + uiSchema: { + title: '{{t("Superior department")}}', + 'x-component': 'DepartmentSelect', + // 'x-component-props': { + // multiple: false, + // fieldNames: { + // label: 'title', + // value: 'id', + // }, + // }, + }, + }, + { + interface: 'm2m', + type: 'belongsToMany', + name: 'roles', + target: 'roles', + collectionName: 'departments', + through: 'departmentsRoles', + foreignKey: 'departmentId', + otherKey: 'roleName', + targetKey: 'name', + sourceKey: 'id', + uiSchema: { + title: '{{t("Roles")}}', + 'x-component': 'AssociationField', + 'x-component-props': { + multiple: true, + fieldNames: { + label: 'title', + value: 'name', + }, + }, + }, + }, + { + interface: 'm2m', + type: 'belongsToMany', + name: 'owners', + collectionName: 'departments', + target: 'users', + through: 'departmentsUsers', + foreignKey: 'departmentId', + otherKey: 'userId', + targetKey: 'id', + sourceKey: 'id', + scope: { + isOwner: true, + }, + uiSchema: { + title: '{{t("Owners")}}', + 'x-component': 'AssociationField', + 'x-component-props': { + multiple: true, + fieldNames: { + label: 'nickname', + value: 'id', + }, + }, + }, + }, + ], +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/collections/users.ts b/packages/plugins/@nocobase/plugin-departments/src/client/collections/users.ts new file mode 100644 index 0000000000..ba309455a7 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/collections/users.ts @@ -0,0 +1,142 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +export const userCollection = { + name: 'users', + fields: [ + { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true }, + interface: 'id', + }, + { + interface: 'input', + type: 'string', + name: 'nickname', + uiSchema: { + type: 'string', + title: '{{t("Nickname")}}', + 'x-component': 'Input', + }, + }, + { + interface: 'input', + type: 'string', + name: 'username', + unique: true, + uiSchema: { + type: 'string', + title: '{{t("Username")}}', + 'x-component': 'Input', + 'x-validator': { username: true }, + required: true, + }, + }, + { + interface: 'email', + type: 'string', + name: 'email', + unique: true, + uiSchema: { + type: 'string', + title: '{{t("Email")}}', + 'x-component': 'Input', + 'x-validator': 'email', + required: true, + }, + }, + { + interface: 'phone', + type: 'string', + name: 'phone', + unique: true, + uiSchema: { + type: 'string', + title: '{{t("Phone")}}', + 'x-component': 'Input', + 'x-validator': 'phone', + required: true, + }, + }, + { + interface: 'm2m', + type: 'belongsToMany', + name: 'roles', + target: 'roles', + foreignKey: 'userId', + otherKey: 'roleName', + onDelete: 'CASCADE', + sourceKey: 'id', + targetKey: 'name', + through: 'rolesUsers', + uiSchema: { + type: 'array', + title: '{{t("Roles")}}', + 'x-component': 'AssociationField', + 'x-component-props': { + multiple: true, + fieldNames: { + label: 'title', + value: 'name', + }, + }, + }, + }, + { + name: 'departments', + type: 'belongsToMany', + interface: 'm2m', + target: 'departments', + foreignKey: 'userId', + otherKey: 'departmentId', + onDelete: 'CASCADE', + sourceKey: 'id', + targetKey: 'id', + through: 'departmentsUsers', + uiSchema: { + type: 'array', + title: '{{t("Departments")}}', + 'x-component': 'DepartmentField', + }, + }, + { + interface: 'm2m', + type: 'belongsToMany', + name: 'mainDepartment', + target: 'departments', + foreignKey: 'userId', + otherKey: 'departmentId', + onDelete: 'CASCADE', + sourceKey: 'id', + targetKey: 'id', + through: 'departmentsUsers', + throughScope: { + isMain: true, + }, + uiSchema: { + type: 'array', + title: '{{t("Main department")}}', + 'x-component': 'DepartmentField', + }, + }, + ], +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/DepartmentOwnersField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/components/DepartmentOwnersField.tsx new file mode 100644 index 0000000000..afcb651250 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/DepartmentOwnersField.tsx @@ -0,0 +1,35 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { SchemaSettings } from '@nocobase/client'; +import { enableLink, fieldComponent, titleField } from './fieldSettings'; + +export const DepartmentOwnersFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:DepartmentOwnersField', + items: [ + { + ...fieldComponent, + }, + { + ...titleField, + }, + { + ...enableLink, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/ReadOnlyAssociationField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/components/ReadOnlyAssociationField.tsx new file mode 100644 index 0000000000..d843d62847 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/ReadOnlyAssociationField.tsx @@ -0,0 +1,27 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React from 'react'; +import { useDepartmentTranslation } from '../locale'; +import { AssociationField } from '@nocobase/client'; +import { connect, mapReadPretty } from '@formily/react'; + +export const ReadOnlyAssociationField = connect(() => { + const { t } = useDepartmentTranslation(); + return
{t('This field is currently not supported for use in form blocks.')}
; +}, mapReadPretty(AssociationField.ReadPretty)); diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/UserDepartmentsField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/components/UserDepartmentsField.tsx new file mode 100644 index 0000000000..e866411db2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/UserDepartmentsField.tsx @@ -0,0 +1,35 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { SchemaSettings } from '@nocobase/client'; +import { enableLink, fieldComponent, titleField } from './fieldSettings'; + +export const UserDepartmentsFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:UserDepartmentsField', + items: [ + { + ...fieldComponent, + }, + { + ...titleField, + }, + { + ...enableLink, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/UserMainDepartmentField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/components/UserMainDepartmentField.tsx new file mode 100644 index 0000000000..0e8355e408 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/UserMainDepartmentField.tsx @@ -0,0 +1,35 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { SchemaSettings } from '@nocobase/client'; +import { enableLink, fieldComponent, titleField } from './fieldSettings'; + +export const UserMainDepartmentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:UserMainDepartmentField', + items: [ + { + ...fieldComponent, + }, + { + ...titleField, + }, + { + ...enableLink, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/fieldSettings.ts b/packages/plugins/@nocobase/plugin-departments/src/client/components/fieldSettings.ts new file mode 100644 index 0000000000..bc637afa40 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/fieldSettings.ts @@ -0,0 +1,185 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { + useCollectionField, + useCollectionManager_deprecated, + useCollection_deprecated, + useCompile, + useDesignable, + useFieldComponentName, + useFieldModeOptions, + useIsAddNewForm, + useTitleFieldOptions, +} from '@nocobase/client'; +import { useDepartmentTranslation } from '../locale'; +import { Field } from '@formily/core'; +import { useField, useFieldSchema, ISchema } from '@formily/react'; + +export const titleField: any = { + name: 'titleField', + type: 'select', + useComponentProps() { + const { t } = useDepartmentTranslation(); + const field = useField(); + const { dn } = useDesignable(); + const options = useTitleFieldOptions(); + const { uiSchema, fieldSchema: tableColumnSchema, collectionField: tableColumnField } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const targetCollectionField = useCollectionField(); + const collectionField = tableColumnField || targetCollectionField; + const fieldNames = { + ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], + ...field?.componentProps?.fieldNames, + ...fieldSchema?.['x-component-props']?.['fieldNames'], + }; + return { + title: t('Title field'), + options, + value: fieldNames?.label, + onChange(label) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const newFieldNames = { + ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], + ...fieldSchema['x-component-props']?.['fieldNames'], + label, + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = newFieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps.fieldNames = fieldSchema['x-component-props']?.fieldNames; + const path = field.path?.splice(field.path?.length - 1, 1); + field.form.query(`${path.concat(`*.` + fieldSchema.name)}`).forEach((f) => { + f.componentProps.fieldNames = fieldNames; + }); + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const isCollectionFieldComponent = (schema: ISchema) => { + return schema['x-component'] === 'CollectionField'; +}; + +const useColumnSchema = () => { + const { getField } = useCollection_deprecated(); + const compile = useCompile(); + const columnSchema = useFieldSchema(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const fieldSchema = columnSchema.reduceProperties((buf, s) => { + if (isCollectionFieldComponent(s)) { + return s; + } + return buf; + }, null); + if (!fieldSchema) { + return {}; + } + + const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']); + return { columnSchema, fieldSchema, collectionField, uiSchema: compile(collectionField?.uiSchema) }; +}; + +export const enableLink = { + name: 'enableLink', + type: 'switch', + useVisible() { + const field = useField(); + return field.readPretty; + }, + useComponentProps() { + const { t } = useDepartmentTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const { dn } = useDesignable(); + return { + title: t('Enable link'), + checked: fieldSchema['x-component-props']?.enableLink !== false, + onChange(flag) { + fieldSchema['x-component-props'] = { + ...fieldSchema?.['x-component-props'], + enableLink: flag, + }; + field.componentProps['enableLink'] = flag; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-component-props': { + ...fieldSchema?.['x-component-props'], + }, + }, + }); + dn.refresh(); + }, + }; + }, +}; + +export const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useDepartmentTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema, collectionField } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const fieldModeOptions = useFieldModeOptions({ fieldSchema: tableColumnSchema, collectionField }); + // const isAddNewForm = useIsAddNewForm(); + // const fieldMode = useFieldComponentName(); + const { dn } = useDesignable(); + return { + title: t('Field component'), + options: fieldModeOptions, + value: 'Select', + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + // if (isSubMode(fieldSchema) && isAddNewForm) { + // // @ts-ignore + // schema.default = null; + // fieldSchema.default = null; + // field?.setInitialValue?.(null); + // field?.setValue?.(null); + // } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/index.ts b/packages/plugins/@nocobase/plugin-departments/src/client/components/index.ts new file mode 100644 index 0000000000..a9a4c4c641 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/index.ts @@ -0,0 +1,22 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +export * from './ReadOnlyAssociationField'; +export * from './UserDepartmentsField'; +export * from './UserMainDepartmentField'; +export * from './DepartmentOwnersField'; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/AggregateSearch.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/AggregateSearch.tsx new file mode 100644 index 0000000000..fccec99e2b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/AggregateSearch.tsx @@ -0,0 +1,216 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React, { useContext } from 'react'; +import { Input, Button, Empty, MenuProps, Dropdown, theme } from 'antd'; +import { useDepartmentTranslation } from '../locale'; +import { createStyles, useAPIClient, useRequest } from '@nocobase/client'; +import { ResourcesContext } from '../ResourcesProvider'; + +const useStyles = createStyles(({ css }) => { + return { + searchDropdown: css` + .ant-dropdown-menu { + max-height: 500px; + overflow-y: scroll; + } + `, + }; +}); + +export const AggregateSearch: React.FC = () => { + const { t } = useDepartmentTranslation(); + const { token } = theme.useToken(); + const { setDepartment, setUser } = useContext(ResourcesContext); + const [open, setOpen] = React.useState(false); + const [keyword, setKeyword] = React.useState(''); + const [users, setUsers] = React.useState([]); + const [departments, setDepartments] = React.useState([]); + const [moreUsers, setMoreUsers] = React.useState(true); + const [moreDepartments, setMoreDepartments] = React.useState(true); + const { styles } = useStyles(); + const limit = 10; + + const api = useAPIClient(); + const service = useRequest( + (params) => + api + .resource('departments') + .aggregateSearch(params) + .then((res) => res?.data?.data), + { + manual: true, + onSuccess: (data, params) => { + const { + values: { type }, + } = params[0] || {}; + if (!data) { + return; + } + if ((!type || type === 'user') && data['users'].length < limit) { + setMoreUsers(false); + } + if ((!type || type === 'department') && data['departments'].length < limit) { + setMoreDepartments(false); + } + setUsers((users) => [...users, ...data['users']]); + setDepartments((departments) => [...departments, ...data['departments']]); + }, + }, + ); + const { run } = service; + + const handleSearch = (keyword: string) => { + setKeyword(keyword); + setUsers([]); + setDepartments([]); + setMoreUsers(true); + setMoreDepartments(true); + if (!keyword) { + return; + } + run({ + values: { keyword, limit }, + }); + setOpen(true); + }; + + const handleChange = (e) => { + if (e.target.value) { + return; + } + setUser(null); + setKeyword(''); + setOpen(false); + service.mutate({}); + setUsers([]); + setDepartments([]); + }; + + const getTitle = (department: any) => { + const title = department.title; + const parent = department.parent; + if (parent) { + return getTitle(parent) + ' / ' + title; + } + return title; + }; + + const LoadMore: React.FC<{ type: string; last: number }> = (props) => { + return ( + + ); + }; + + const getItems = () => { + const items: MenuProps['items'] = []; + if (!users.length && !departments.length) { + return [ + { + key: '0', + label: , + disabled: true, + }, + ]; + } + if (users.length) { + items.push({ + key: '0', + type: 'group', + label: t('Users'), + children: users.map((user: { nickname: string; username: string; phone?: string; email?: string }) => ({ + key: user.username, + label: ( +
setUser(user)}> +
{user.nickname || user.username}
+
+ {`${user.username}${user.phone ? ' | ' + user.phone : ''}${user.email ? ' | ' + user.email : ''}`} +
+
+ ), + })), + }); + if (moreUsers) { + items.push({ + type: 'group', + key: '0-loadMore', + label: , + }); + } + } + if (departments.length) { + items.push({ + key: '1', + type: 'group', + label: t('Departments'), + children: departments.map((department: any) => ({ + key: department.id, + label:
setDepartment(department)}>{getTitle(department)}
, + })), + }); + if (moreDepartments) { + items.push({ + type: 'group', + key: '1-loadMore', + label: , + }); + } + } + return items; + }; + + return ( + setOpen(open)} + > + { + if (!keyword) { + setOpen(false); + } + }} + onFocus={() => setDepartment(null)} + onSearch={handleSearch} + onChange={handleChange} + placeholder={t('Search for departments, users')} + style={{ marginBottom: '20px' }} + /> + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/Department.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/Department.tsx new file mode 100644 index 0000000000..b8673777ab --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/Department.tsx @@ -0,0 +1,149 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React, { createContext, useContext, useState } from 'react'; +import { Row, Button, Divider, theme } from 'antd'; +import { UserOutlined } from '@ant-design/icons'; +import { useDepartmentTranslation } from '../locale'; +import { NewDepartment } from './NewDepartment'; +import { DepartmentTree } from './DepartmentTree'; +import { ResourcesContext } from '../ResourcesProvider'; +import { AggregateSearch } from './AggregateSearch'; +import { useDepartmentManager } from '../hooks'; +import { + ActionContextProvider, + RecordProvider, + SchemaComponent, + SchemaComponentOptions, + useAPIClient, + useActionContext, + useRecord, + useResourceActionContext, +} from '@nocobase/client'; +import { useForm, useField } from '@formily/react'; +import { DepartmentOwnersField } from './DepartmentOwnersField'; + +export const DepartmentTreeContext = createContext({} as ReturnType); + +export const useCreateDepartment = () => { + const form = useForm(); + const field = useField(); + const ctx = useActionContext(); + const { refreshAsync } = useResourceActionContext(); + const api = useAPIClient(); + const { expandedKeys, setLoadedKeys, setExpandedKeys } = useContext(DepartmentTreeContext); + return { + async run() { + try { + await form.submit(); + field.data = field.data || {}; + field.data.loading = true; + await api.resource('departments').create({ values: form.values }); + ctx.setVisible(false); + await form.reset(); + field.data.loading = false; + const expanded = [...expandedKeys]; + setLoadedKeys([]); + setExpandedKeys([]); + await refreshAsync(); + setExpandedKeys(expanded); + } catch (error) { + if (field.data) { + field.data.loading = false; + } + } + }, + }; +}; + +export const useUpdateDepartment = () => { + const field = useField(); + const form = useForm(); + const ctx = useActionContext(); + const { refreshAsync } = useResourceActionContext(); + const api = useAPIClient(); + const { id: filterByTk } = useRecord() as any; + const { expandedKeys, setLoadedKeys, setExpandedKeys } = useContext(DepartmentTreeContext); + const { department, setDepartment } = useContext(ResourcesContext); + return { + async run() { + await form.submit(); + field.data = field.data || {}; + field.data.loading = true; + try { + await api.resource('departments').update({ filterByTk, values: form.values }); + setDepartment({ department, ...form.values }); + ctx.setVisible(false); + await form.reset(); + const expanded = [...expandedKeys]; + setLoadedKeys([]); + setExpandedKeys([]); + await refreshAsync(); + setExpandedKeys(expanded); + } catch (e) { + console.log(e); + } finally { + field.data.loading = false; + } + }, + }; +}; + +export const Department: React.FC = () => { + const { t } = useDepartmentTranslation(); + const [visible, setVisible] = useState(false); + const [drawer, setDrawer] = useState({} as any); + const { department, setDepartment } = useContext(ResourcesContext); + const { token } = theme.useToken(); + const departmentManager = useDepartmentManager({ + label: ({ node }) => , + }); + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentBlock.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentBlock.tsx new file mode 100644 index 0000000000..01b379d59a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentBlock.tsx @@ -0,0 +1,40 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React from 'react'; +import { SchemaComponent } from '@nocobase/client'; +import { DepartmentManagement } from './DepartmentManagement'; +import { uid } from '@formily/shared'; + +export const DepartmentBlock: React.FC = () => { + return ( + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentField.tsx new file mode 100644 index 0000000000..76763a15bb --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentField.tsx @@ -0,0 +1,48 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React, { useContext } from 'react'; +import { useField } from '@formily/react'; +import { Field } from '@formily/core'; +import { ResourcesContext } from '../ResourcesProvider'; +import { getDepartmentTitle } from '../utils'; +import { EllipsisWithTooltip } from '@nocobase/client'; + +export const DepartmentField: React.FC = () => { + const { setDepartment } = useContext(ResourcesContext); + const field = useField(); + const values = field.value || []; + const deptsMap = values.reduce((mp: { [id: number]: any }, dept: any) => { + mp[dept.id] = dept; + return mp; + }, {}); + const depts = values.map((dept: { id: number; title: string }, index: number) => ( + + { + e.preventDefault(); + setDepartment(deptsMap[dept.id]); + }} + > + {getDepartmentTitle(dept)} + + {index !== values.length - 1 ? , : ''} + + )); + return {depts}; +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentManagement.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentManagement.tsx new file mode 100644 index 0000000000..5bae972bf8 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentManagement.tsx @@ -0,0 +1,44 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React from 'react'; +import { Col, Row } from 'antd'; +import { Department } from './Department'; +import { Member } from './Member'; +import { SchemaComponentOptions } from '@nocobase/client'; +import { SuperiorDepartmentSelect, DepartmentSelect } from './DepartmentTreeSelect'; +import { DepartmentsListProvider, UsersListProvider } from '../ResourcesProvider'; + +export const DepartmentManagement: React.FC = () => { + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentOwnersField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentOwnersField.tsx new file mode 100644 index 0000000000..6faffb38f3 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentOwnersField.tsx @@ -0,0 +1,115 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { + ActionContextProvider, + ResourceActionProvider, + SchemaComponent, + useActionContext, + useRecord, +} from '@nocobase/client'; +import React, { useEffect, useRef, useState } from 'react'; +import { Select } from 'antd'; +import { Field } from '@formily/core'; +import { useField } from '@formily/react'; +import { departmentOwnersSchema } from './schemas/departments'; + +export const DepartmentOwnersField: React.FC = () => { + const [visible, setVisible] = useState(false); + const department = useRecord() as any; + const field = useField(); + const [value, setValue] = useState([]); + const selectedRows = useRef([]); + const handleSelect = (_: number[], rows: any[]) => { + selectedRows.current = rows; + }; + + const useSelectOwners = () => { + const { setVisible } = useActionContext(); + return { + run() { + const selected = field.value || []; + field.setValue([...selected, ...selectedRows.current]); + selectedRows.current = []; + setVisible(false); + }, + }; + }; + + useEffect(() => { + if (!field.value) { + return; + } + setValue( + field.value.map((owner: any) => ({ + value: owner.id, + label: owner.nickname || owner.username, + })), + ); + }, [field.value]); + + const RequestProvider: React.FC = (props) => ( + owner.id), + }, + } + : {}, + }, + }} + > + {props.children} + + ); + + return ( + + ; + } + console.log(collectionField); + return ( +
+ + + {collectionField?.target && collectionField?.target !== 'attachments' && ( + + + + + + { + return s['x-component'] === 'AssociationField.Selector'; + }} + /> + + + + + + )} + +
+ ); +}; + +const FileManageReadPretty = connect((props) => { + const { value } = props; + const fieldSchema = useFieldSchema(); + const componentMode = fieldSchema?.['x-component-props']?.['componentMode']; + const { getField } = useCollection_deprecated(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema['x-collection-field']); + if (componentMode === 'url') { + return {value}; + } + return ( + {collectionField ? : null} + ); +}); + +export const AttachmentUrl = connect(InnerAttachmentUrl, mapReadPretty(FileManageReadPretty)); diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/hook/index.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/hook/index.ts new file mode 100644 index 0000000000..412cd9c469 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/hook/index.ts @@ -0,0 +1,106 @@ +/** + * 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 { useFieldSchema } from '@formily/react'; +import { useCollectionField, useDesignable, useRequest } from '@nocobase/client'; +import { cloneDeep, uniqBy } from 'lodash'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +function useStorageRules(storage) { + const name = storage ?? ''; + const { loading, data } = useRequest( + { + url: `storages:getBasicInfo/${name}`, + }, + { + refreshDeps: [name], + }, + ); + return (!loading && data?.data) || null; +} +export function useAttachmentUrlFieldProps(props) { + const field = useCollectionField(); + const rules = useStorageRules(field?.storage); + return { + ...props, + rules, + action: `${field.target}:create${field.storage ? `?attachmentField=${field.collectionName}.${field.name}` : ''}`, + toValueItem: (data) => { + return data?.thumbnailRule ? `${data?.url}${data?.thumbnailRule}` : data?.url; + }, + getThumbnailURL: (file) => { + return file?.url; + }, + }; +} + +export const useInsertSchema = (component) => { + const fieldSchema = useFieldSchema(); + const { insertAfterBegin } = useDesignable(); + const insert = useCallback( + (ss) => { + const schema = fieldSchema.reduceProperties((buf, s) => { + if (s['x-component'] === 'AssociationField.' + component) { + return s; + } + return buf; + }, null); + if (!schema) { + insertAfterBegin(cloneDeep(ss)); + } + }, + [component, fieldSchema, insertAfterBegin], + ); + return insert; +}; + +export const useAttachmentTargetProps = () => { + const { t } = useTranslation(); + // TODO(refactor): whitelist should be changed to storage property,url is signed by plugin-s3-pro, this enmus is from plugin-file-manager + const buildInStorage = ['local', 'ali-oss', 's3', 'tx-cos']; + return { + service: { + resource: 'collections', + params: { + filter: { + 'options.template': 'file', + }, + paginate: false, + }, + }, + manual: false, + fieldNames: { + label: 'title', + value: 'name', + }, + mapOptions: (value) => { + if (value.name === 'attachments') { + return { + ...value, + title: t('Attachments'), + }; + } + return value; + }, + toOptionsItem: (data) => { + data.unshift({ + name: 'attachments', + title: t('Attachments'), + }); + return uniqBy( + data.filter((v) => v.name), + 'name', + ); + }, + optionFilter: (option) => { + return !option.storage || buildInStorage.includes(option.storage); + }, + }; +}; diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/index.tsx b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/index.tsx new file mode 100644 index 0000000000..c535c97f67 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/index.tsx @@ -0,0 +1,38 @@ +/** + * 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 { Plugin, lazy } from '@nocobase/client'; +import { AttachmentURLFieldInterface } from './interfaces/attachment-url'; +import { useAttachmentUrlFieldProps } from './hook'; +// import { AttachmentUrl } from './component/AttachmentUrl'; +const { AttachmentUrl } = lazy(() => import('./component/AttachmentUrl'), 'AttachmentUrl'); + +import { attachmentUrlComponentFieldSettings } from './settings'; +export class PluginFieldAttachmentUrlClient extends Plugin { + async afterAdd() { + // await this.app.pm.add() + } + + async beforeLoad() {} + + // You can get and modify the app instance here + async load() { + this.app.dataSourceManager.addFieldInterfaces([AttachmentURLFieldInterface]); + this.app.addScopes({ useAttachmentUrlFieldProps }); + + this.app.addComponents({ AttachmentUrl }); + this.app.schemaSettingsManager.add(attachmentUrlComponentFieldSettings); + + // this.app.addProvider() + // this.app.addProviders() + // this.app.router.add() + } +} + +export default PluginFieldAttachmentUrlClient; diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/interfaces/attachment-url.tsx b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/interfaces/attachment-url.tsx new file mode 100644 index 0000000000..781da59f6b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/interfaces/attachment-url.tsx @@ -0,0 +1,79 @@ +/** + * 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 { CollectionFieldInterface, interfacesProperties } from '@nocobase/client'; +import { ISchema } from '@formily/react'; +import { useAttachmentTargetProps } from '../hook'; +import { tStr } from '../locale'; + +const { defaultProps, operators } = interfacesProperties; + +export const defaultToolbar = [ + 'headings', + 'bold', + 'italic', + 'strike', + 'link', + 'list', + 'ordered-list', + 'check', + 'quote', + 'line', + 'code', + 'inline-code', + 'upload', + 'fullscreen', +]; + +export class AttachmentURLFieldInterface extends CollectionFieldInterface { + name = 'attachmentURL'; + type = 'object'; + group = 'media'; + title = tStr('Attachment (URL)'); + default = { + type: 'string', + // name, + uiSchema: { + type: 'string', + // title, + 'x-component': 'AttachmentUrl', + 'x-use-component-props': 'useAttachmentUrlFieldProps', + }, + }; + availableTypes = ['string', 'text']; + properties = { + ...defaultProps, + target: { + required: true, + default: 'attachments', + type: 'string', + title: tStr('Which file collection should it be uploaded to'), + 'x-decorator': 'FormItem', + 'x-component': 'RemoteSelect', + 'x-use-component-props': useAttachmentTargetProps, + }, + targetKey: { + 'x-hidden': true, + default: 'id', + type: 'string', + }, + }; + schemaInitialize(schema: ISchema, { block }) { + schema['x-component-props'] = schema['x-component-props'] || {}; + schema['x-component-props']['mode'] = 'AttachmentUrl'; + if (['Table', 'Kanban'].includes(block)) { + schema['x-component-props']['ellipsis'] = true; + schema['x-component-props']['size'] = 'small'; + } + } + filterable = { + operators: operators.bigField, + }; + titleUsable = true; +} diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/locale.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/locale.ts new file mode 100644 index 0000000000..a26dd0158f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/locale.ts @@ -0,0 +1,30 @@ +/** + * 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. + */ + +// @ts-ignore +import pkg from '../../package.json'; +import { useApp } from '@nocobase/client'; +import { useTranslation } from 'react-i18next'; + +export const NAMESPACE = 'attachmentUrl'; + +export function useT() { + const app = useApp(); + return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] }); +} + +export function tStr(key: string) { + return `{{t(${JSON.stringify(key)}, { ns: ['${pkg.name}', 'client'], nsMode: 'fallback' })}}`; +} + +export function useAttachmentUrlTranslation() { + return useTranslation([NAMESPACE, 'client'], { + nsMode: 'fallback', + }); +} diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/schema.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/schema.ts new file mode 100644 index 0000000000..f5ae80d0ce --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/schema.ts @@ -0,0 +1,53 @@ +/** + * 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. + */ + +export default { + Selector: { + type: 'void', + 'x-component': 'AssociationField.Selector', + title: '{{ t("Select record") }}', + 'x-component-props': { + className: 'nb-record-picker-selector', + }, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:tableSelector:addBlock', + properties: {}, + }, + footer: { + 'x-component': 'Action.Container.Footer', + 'x-component-props': {}, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-component-props': {}, + properties: { + submit: { + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': 'usePickActionProps', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:submit', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/settings/index.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/settings/index.ts new file mode 100644 index 0000000000..52463073c2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/settings/index.ts @@ -0,0 +1,171 @@ +/** + * 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 { Field } from '@formily/core'; +import { useField, useFieldSchema, useForm } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { useColumnSchema, useIsFieldReadPretty, SchemaSettings, useDesignable } from '@nocobase/client'; + +const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const { dn } = useDesignable(); + + return { + title: t('Field component'), + options: [ + { label: t('URL'), value: 'url' }, + { label: t('Preview'), value: 'preview' }, + ], + value: fieldSchema['x-component-props']['componentMode'] || 'preview', + onChange(componentMode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['componentMode'] = componentMode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.componentMode = componentMode; + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, + useVisible() { + const readPretty = useIsFieldReadPretty(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + return readPretty; + }, +}; + +export const attachmentUrlComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:AttachmentUrl', + items: [ + { + name: 'quickUpload', + type: 'switch', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const { dn, refresh } = useDesignable(); + return { + title: t('Quick upload'), + checked: fieldSchema['x-component-props']?.quickUpload !== (false as boolean), + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.componentProps.quickUpload = value; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].quickUpload = value; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + useVisible() { + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const field = useField(); + const form = useForm(); + const isReadPretty = tableColumnSchema?.['x-read-pretty'] || field.readPretty || form.readPretty; + return !isReadPretty && !field.componentProps.underFilter; + }, + }, + { + name: 'selectFile', + type: 'switch', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const { dn, refresh } = useDesignable(); + return { + title: t('Select file'), + checked: fieldSchema['x-component-props']?.selectFile !== (false as boolean), + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.componentProps.selectFile = value; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].selectFile = value; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + useVisible() { + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const field = useField(); + const form = useForm(); + const isReadPretty = tableColumnSchema?.['x-read-pretty'] || field.readPretty || form.readPretty; + return !isReadPretty && !field.componentProps.underFilter; + }, + }, + fieldComponent, + { + name: 'size', + type: 'select', + useVisible() { + const readPretty = useIsFieldReadPretty(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + return readPretty && !tableColumnSchema; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + return { + title: t('Size'), + options: [ + { label: t('Large'), value: 'large' }, + { label: t('Default'), value: 'default' }, + { label: t('Small'), value: 'small' }, + ], + value: field?.componentProps?.size || 'default', + onChange(size) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['size'] = size; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.size = size; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/index.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/index.ts new file mode 100644 index 0000000000..be99a2ff1a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/index.ts @@ -0,0 +1,11 @@ +/** + * 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. + */ + +export * from './server'; +export { default } from './server'; diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/en-US.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/en-US.json @@ -0,0 +1 @@ +{} diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/zh-CN.json new file mode 100644 index 0000000000..78def6ea14 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/zh-CN.json @@ -0,0 +1,4 @@ +{ + "Which file collection should it be uploaded to":"上传到文件表", + "Attachment (URL)":"附件 (URL)" +} diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/collections/.gitkeep b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/collections/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/index.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/index.ts new file mode 100644 index 0000000000..be989de7c3 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/index.ts @@ -0,0 +1,10 @@ +/** + * 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. + */ + +export { default } from './plugin'; diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/plugin.ts new file mode 100644 index 0000000000..372274c091 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/plugin.ts @@ -0,0 +1,28 @@ +/** + * 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 { Plugin } from '@nocobase/server'; + +export class PluginFieldAttachmentUrlServer extends Plugin { + async afterAdd() {} + + async beforeLoad() {} + + async load() {} + + async install() {} + + async afterEnable() {} + + async afterDisable() {} + + async remove() {} +} + +export default PluginFieldAttachmentUrlServer; diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/.npmignore b/packages/plugins/@nocobase/plugin-workflow-response-message/.npmignore new file mode 100644 index 0000000000..65f5e8779f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/.npmignore @@ -0,0 +1,2 @@ +/node_modules +/src diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/README.md b/packages/plugins/@nocobase/plugin-workflow-response-message/README.md new file mode 100644 index 0000000000..8ced21e948 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/README.md @@ -0,0 +1 @@ +# @nocobase/plugin-workflow-response-message diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/client.d.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/client.d.ts new file mode 100644 index 0000000000..6c459cbac4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/client.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client'; +export { default } from './dist/client'; diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/client.js b/packages/plugins/@nocobase/plugin-workflow-response-message/client.js new file mode 100644 index 0000000000..b6e3be70e6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/client.js @@ -0,0 +1 @@ +module.exports = require('./dist/client/index.js'); diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/package.json b/packages/plugins/@nocobase/plugin-workflow-response-message/package.json new file mode 100644 index 0000000000..286159d0d0 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nocobase/plugin-workflow-response-message", + "version": "1.6.19", + "displayName": "Workflow: Response message", + "displayName.zh-CN": "工作流:响应消息", + "description": "Used for assemble response message and showing to client in form event and request interception workflows.", + "description.zh-CN": "用于在表单事件和请求拦截工作流中组装并向客户端显示响应消息。", + "main": "dist/server/index.js", + "homepage": "https://docs.nocobase.com/handbook/workflow-response-message", + "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/workflow-response-message", + "peerDependencies": { + "@nocobase/client": "1.x", + "@nocobase/server": "1.x", + "@nocobase/test": "1.x", + "@nocobase/utils": "1.x" + }, + "devDependencies": { + "@nocobase/plugin-workflow": "1.x" + }, + "keywords": [ + "Workflow" + ], + "gitHead": "080fc78c1a744d47e010b3bbe5840446775800e4" +} diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/server.d.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/server.d.ts new file mode 100644 index 0000000000..c41081ddc6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/server.d.ts @@ -0,0 +1,2 @@ +export * from './dist/server'; +export { default } from './dist/server'; diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/server.js b/packages/plugins/@nocobase/plugin-workflow-response-message/server.js new file mode 100644 index 0000000000..972842039a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/server.js @@ -0,0 +1 @@ +module.exports = require('./dist/server/index.js'); diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/ResponseMessageInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/ResponseMessageInstruction.tsx new file mode 100644 index 0000000000..efdd6a7241 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/ResponseMessageInstruction.tsx @@ -0,0 +1,87 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React from 'react'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Alert, Space } from 'antd'; + +import { + Instruction, + RadioWithTooltip, + WorkflowVariableInput, + WorkflowVariableTextArea, +} from '@nocobase/plugin-workflow/client'; + +import { NAMESPACE } from '../locale'; + +export default class extends Instruction { + title = `{{t("Response message", { ns: "${NAMESPACE}" })}}`; + type = 'response-message'; + group = 'extended'; + description = `{{t("Add response message, will be send to client when process of request ends.", { ns: "${NAMESPACE}" })}}`; + icon = (); + fieldset = { + message: { + type: 'string', + title: `{{t("Message content", { ns: "${NAMESPACE}" })}}`, + description: `{{t('Supports variables in template.', { ns: "${NAMESPACE}", name: '{{name}}' })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'WorkflowVariableTextArea', + }, + info: { + type: 'void', + 'x-component': 'Space', + 'x-component-props': { + direction: 'vertical', + }, + properties: { + success: { + type: 'void', + 'x-component': 'Alert', + 'x-component-props': { + type: 'success', + showIcon: true, + description: `{{t('If the workflow ends normally, the response message will return a success status by default.', { ns: "${NAMESPACE}" })}}`, + }, + }, + failure: { + type: 'void', + 'x-component': 'Alert', + 'x-component-props': { + type: 'error', + showIcon: true, + description: `{{t('If you want to return a failure status, please add an "End Process" node downstream to terminate the workflow.', { ns: "${NAMESPACE}" })}}`, + }, + }, + }, + }, + }; + scope = {}; + components = { + RadioWithTooltip, + WorkflowVariableTextArea, + WorkflowVariableInput, + Alert, + Space, + }; + isAvailable({ workflow, upstream, branchIndex }) { + return ( + workflow.type === 'request-interception' || (['action', 'custom-action'].includes(workflow.type) && workflow.sync) + ); + } +} diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/index.tsx b/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/index.tsx new file mode 100644 index 0000000000..258f6c8d8b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/index.tsx @@ -0,0 +1,31 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { Plugin } from '@nocobase/client'; +import WorkflowPlugin from '@nocobase/plugin-workflow/client'; + +import ResponseMessageInstruction from './ResponseMessageInstruction'; + +export class PluginWorkflowResponseMessageClient extends Plugin { + async load() { + const workflowPlugin = this.app.pm.get('workflow') as WorkflowPlugin; + workflowPlugin.registerInstruction('response-message', ResponseMessageInstruction); + } +} + +export default PluginWorkflowResponseMessageClient; diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/index.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/index.ts new file mode 100644 index 0000000000..7d69462f4f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/index.ts @@ -0,0 +1,20 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +export * from './server'; +export { default } from './server'; diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/en-US.json new file mode 100644 index 0000000000..11832d6610 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/en-US.json @@ -0,0 +1,6 @@ +{ + "Response message": "Response message", + "Add response message, will be send to client when process of request ends.": "Add response message, will be send to client when process of request ends.", + "Message content": "Message content", + "Supports variables in template.": "Supports variables in template." +} diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/index.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/index.ts new file mode 100644 index 0000000000..543b392f21 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/index.ts @@ -0,0 +1,25 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { i18n } from '@nocobase/client'; + +export const NAMESPACE = '@nocobase/plugin-workflow-response-message'; + +export function lang(key: string, options = {}) { + return i18n.t(key, { ...options, ns: NAMESPACE }); +} diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/zh-CN.json new file mode 100644 index 0000000000..c8b986cace --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/zh-CN.json @@ -0,0 +1,8 @@ +{ + "Response message": "响应消息", + "Add response message, will be send to client when process of request ends.": "添加响应消息,将在请求处理结束时发送给客户端。", + "Message content": "消息内容", + "Supports variables in template.": "支持模板变量。", + "If the workflow ends normally, the response message will return a success status by default.": "如果工作流正常结束,响应消息默认返回成功状态。", + "If you want to return a failure status, please add an \"End Process\" node downstream to terminate the workflow.": "如果希望返回失败状态,请在下游添加“结束流程”节点终止工作流。" +} diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/Plugin.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/Plugin.ts new file mode 100644 index 0000000000..cdd6eece72 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/Plugin.ts @@ -0,0 +1,31 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { Plugin } from '@nocobase/server'; +import PluginWorkflowServer from '@nocobase/plugin-workflow'; + +import ResponseMessageInstruction from './ResponseMessageInstruction'; + +export class PluginWorkflowResponseMessageServer extends Plugin { + async load() { + const workflowPlugin = this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer; + workflowPlugin.registerInstruction('response-message', ResponseMessageInstruction); + } +} + +export default PluginWorkflowResponseMessageServer; diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/ResponseMessageInstruction.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/ResponseMessageInstruction.ts new file mode 100644 index 0000000000..e277a44e35 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/ResponseMessageInstruction.ts @@ -0,0 +1,55 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { Instruction, Processor, JOB_STATUS, FlowNodeModel } from '@nocobase/plugin-workflow'; + +interface Config { + message?: string; +} + +export default class extends Instruction { + async run(node: FlowNodeModel, prevJob, processor: Processor) { + const { httpContext } = processor.options; + + if (!httpContext) { + return { + status: JOB_STATUS.RESOLVED, + result: null, + }; + } + + if (!httpContext.state) { + httpContext.state = {}; + } + + if (!httpContext.state.messages) { + httpContext.state.messages = []; + } + + const message = processor.getParsedValue(node.config.message, node.id); + + if (message) { + httpContext.state.messages.push({ message }); + } + + return { + status: JOB_STATUS.RESOLVED, + result: message, + }; + } +} diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/instruction.test.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/instruction.test.ts new file mode 100644 index 0000000000..500a70cefe --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/instruction.test.ts @@ -0,0 +1,346 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import Database from '@nocobase/database'; +import { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow'; +import { getApp } from '@nocobase/plugin-workflow-test'; +import { MockServer } from '@nocobase/test'; + +import Plugin from '..'; + +describe.skip('workflow > instructions > response-message', () => { + let app: MockServer; + let db: Database; + let PostRepo; + let WorkflowModel; + let workflow; + let users; + let userAgents; + + beforeEach(async () => { + app = await getApp({ + plugins: ['users', 'auth', 'error-handler', 'workflow-request-interceptor', Plugin], + }); + + db = app.db; + + PostRepo = db.getCollection('posts').repository; + + WorkflowModel = db.getModel('workflows'); + workflow = await WorkflowModel.create({ + enabled: true, + type: 'request-interception', + config: { + global: true, + actions: ['create'], + collection: 'posts', + }, + }); + + const UserModel = db.getCollection('users').model; + users = await UserModel.bulkCreate([ + { id: 2, nickname: 'a' }, + { id: 3, nickname: 'b' }, + ]); + + userAgents = await Promise.all(users.map((user) => app.agent().login(user))); + }); + + afterEach(() => app.destroy()); + + describe('no end, pass flow', () => { + it('no message', async () => { + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(200); + expect(res1.body).toMatchObject({ data: { title: 't1' } }); + expect(res1.body.messages).toBeUndefined(); + + const post = await PostRepo.findOne(); + expect(post).toBeDefined(); + expect(post.title).toBe('t1'); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(0); + }); + + it('has node, but null message', async () => { + const n1 = await workflow.createNode({ + type: 'response-message', + }); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(200); + expect(res1.body).toMatchObject({ data: { title: 't1' } }); + expect(res1.body.messages).toBeUndefined(); + + const post = await PostRepo.findOne(); + expect(post).toBeDefined(); + expect(post.title).toBe('t1'); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(1); + }); + + it('has node, but empty message', async () => { + const n1 = await workflow.createNode({ + type: 'response-message', + config: { + message: '', + }, + }); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(200); + expect(res1.body).toMatchObject({ data: { title: 't1' } }); + expect(res1.body.messages).toBeUndefined(); + + const post = await PostRepo.findOne(); + expect(post).toBeDefined(); + expect(post.title).toBe('t1'); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(1); + }); + + it('single static message', async () => { + const n1 = await workflow.createNode({ + type: 'response-message', + config: { + message: 'm1', + }, + }); + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(200); + expect(res1.body).toMatchObject({ data: { title: 't1' } }); + expect(res1.body.messages).toEqual([{ message: 'm1' }]); + + const post = await PostRepo.findOne(); + expect(post).toBeDefined(); + expect(post.title).toBe('t1'); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(1); + }); + + it('multiple static messages', async () => { + const n1 = await workflow.createNode({ + type: 'response-message', + config: { + message: 'm1', + }, + }); + const n2 = await workflow.createNode({ + type: 'response-message', + config: { + message: 'm2', + }, + upstreamId: n1.id, + }); + await n1.setDownstream(n2); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(200); + expect(res1.body).toMatchObject({ data: { title: 't1' } }); + expect(res1.body.messages).toEqual([{ message: 'm1' }, { message: 'm2' }]); + + const post = await PostRepo.findOne(); + expect(post).toBeDefined(); + expect(post.title).toBe('t1'); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(2); + }); + + it('single dynamic message', async () => { + const n1 = await workflow.createNode({ + type: 'response-message', + config: { + message: 'new post "{{ $context.params.values.title }}" by {{ $context.user.nickname }}', + }, + }); + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(200); + expect(res1.body).toMatchObject({ data: { title: 't1' } }); + expect(res1.body.messages).toEqual([{ message: `new post "t1" by ${users[0].nickname}` }]); + + const post = await PostRepo.findOne(); + expect(post).toBeDefined(); + expect(post.title).toBe('t1'); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(1); + }); + }); + + describe('end as success', () => { + it('no message', async () => { + const n1 = await workflow.createNode({ + type: 'end', + config: { + endStatus: JOB_STATUS.RESOLVED, + }, + }); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(200); + expect(res1.body.data).toBeUndefined(); + expect(res1.body.messages).toBeUndefined(); + + const post = await PostRepo.findOne(); + expect(post).toBeNull(); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(1); + + const posts = await PostRepo.find(); + expect(posts.length).toBe(0); + }); + + it('single static message', async () => { + const n1 = await workflow.createNode({ + type: 'response-message', + config: { + message: 'm1', + }, + }); + + const n2 = await workflow.createNode({ + type: 'end', + config: { + endStatus: JOB_STATUS.RESOLVED, + }, + upstreamId: n1.id, + }); + + await n1.setDownstream(n2); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(200); + expect(res1.body.data).toBeUndefined(); + expect(res1.body.messages).toEqual([{ message: 'm1' }]); + + const post = await PostRepo.findOne(); + expect(post).toBeNull(); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(2); + }); + }); + + describe('end as failure', () => { + it('no message', async () => { + const n1 = await workflow.createNode({ + type: 'end', + config: { + endStatus: JOB_STATUS.FAILED, + }, + }); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(400); + expect(res1.body.data).toBeUndefined(); + expect(res1.body.messages).toBeUndefined(); + + const post = await PostRepo.findOne(); + expect(post).toBeNull(); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.FAILED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(1); + }); + + it('single static message', async () => { + const n1 = await workflow.createNode({ + type: 'response-message', + config: { + message: 'm1', + }, + }); + + const n2 = await workflow.createNode({ + type: 'end', + config: { + endStatus: JOB_STATUS.FAILED, + }, + upstreamId: n1.id, + }); + + await n1.setDownstream(n2); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + }); + + expect(res1.status).toBe(400); + expect(res1.body.data).toBeUndefined(); + expect(res1.body.errors).toEqual([{ message: 'm1' }]); + + const post = await PostRepo.findOne(); + expect(post).toBeNull(); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.FAILED); + const jobs = await e1.getJobs(); + expect(jobs.length).toBe(2); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/multiple.test.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/multiple.test.ts new file mode 100644 index 0000000000..21f207c449 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/multiple.test.ts @@ -0,0 +1,185 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import Database from '@nocobase/database'; +import { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow'; +import { getApp } from '@nocobase/plugin-workflow-test'; +import { MockServer } from '@nocobase/test'; + +import Plugin from '..'; + +describe.skip('workflow > multiple workflows', () => { + let app: MockServer; + let db: Database; + let PostRepo; + let WorkflowModel; + let workflow; + let users; + let userAgents; + + beforeEach(async () => { + app = await getApp({ + plugins: ['users', 'auth', 'error-handler', 'workflow-request-interceptor', Plugin], + }); + + db = app.db; + + PostRepo = db.getCollection('posts').repository; + + WorkflowModel = db.getModel('workflows'); + workflow = await WorkflowModel.create({ + enabled: true, + type: 'request-interception', + config: { + global: true, + actions: ['create'], + collection: 'posts', + }, + }); + + const UserModel = db.getCollection('users').model; + users = await UserModel.bulkCreate([ + { id: 2, nickname: 'a' }, + { id: 3, nickname: 'b' }, + ]); + + userAgents = await Promise.all(users.map((user) => app.agent().login(user))); + }); + + afterEach(() => app.destroy()); + + describe('order', () => { + it('workflow 2 run first and pass, workflow 1 ends as success', async () => { + const n1 = await workflow.createNode({ + type: 'response-message', + config: { + message: 'm1', + }, + }); + const n2 = await workflow.createNode({ + type: 'end', + config: { + endStatus: JOB_STATUS.RESOLVED, + }, + upstreamId: n1.id, + }); + await n1.setDownstream(n2); + + const w2 = await WorkflowModel.create({ + enabled: true, + type: 'request-interception', + config: { + action: 'create', + collection: 'posts', + }, + }); + + const n3 = await w2.createNode({ + type: 'response-message', + config: { + message: 'm2', + }, + }); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + triggerWorkflows: w2.key, + }); + + expect(res1.status).toBe(200); + expect(res1.body.data).toBeUndefined(); + expect(res1.body.messages).toEqual([{ message: 'm2' }, { message: 'm1' }]); + + const post = await PostRepo.findOne(); + expect(post).toBeNull(); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const j1s = await e1.getJobs(); + expect(j1s.length).toBe(2); + + const [e2] = await w2.getExecutions(); + expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED); + const j2s = await e2.getJobs(); + expect(j2s.length).toBe(1); + }); + + it('local workflow in trigger key order', async () => { + const w1 = await WorkflowModel.create({ + enabled: true, + type: 'request-interception', + config: { + action: 'create', + collection: 'posts', + }, + }); + + const n1 = await w1.createNode({ + type: 'response-message', + config: { + message: 'm1', + }, + }); + + const w2 = await WorkflowModel.create({ + enabled: true, + type: 'request-interception', + config: { + action: 'create', + collection: 'posts', + }, + }); + + const n2 = await w2.createNode({ + type: 'response-message', + config: { + message: 'm2', + }, + }); + + const n3 = await w2.createNode({ + type: 'end', + config: { + endStatus: JOB_STATUS.RESOLVED, + }, + upstreamId: n2.id, + }); + + await n2.setDownstream(n3); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + triggerWorkflows: [w2.key, w1.key].join(), + }); + + expect(res1.status).toBe(200); + expect(res1.body.data).toBeUndefined(); + expect(res1.body.messages).toEqual([{ message: 'm2' }]); + + const post = await PostRepo.findOne(); + expect(post).toBeNull(); + + const e1s = await w1.getExecutions(); + expect(e1s.length).toBe(0); + const [e2] = await w2.getExecutions(); + expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await e2.getJobs(); + expect(jobs.length).toBe(2); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/index.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/index.ts new file mode 100644 index 0000000000..b0c269d075 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/index.ts @@ -0,0 +1,19 @@ +/** + * 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. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +export { default } from './Plugin'; diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index d91c789ac8..b9b2f8934d 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -31,8 +31,10 @@ "@nocobase/plugin-data-source-main": "1.6.19", "@nocobase/plugin-data-source-manager": "1.6.19", "@nocobase/plugin-data-visualization": "1.6.19", + "@nocobase/plugin-departments": "1.6.19", "@nocobase/plugin-environment-variables": "1.6.19", "@nocobase/plugin-error-handler": "1.6.19", + "@nocobase/plugin-field-attachment-url": "1.6.19", "@nocobase/plugin-field-china-region": "1.6.19", "@nocobase/plugin-field-formula": "1.6.19", "@nocobase/plugin-field-m2m-array": "1.6.19", @@ -74,6 +76,7 @@ "@nocobase/plugin-workflow-notification": "1.6.19", "@nocobase/plugin-workflow-parallel": "1.6.19", "@nocobase/plugin-workflow-request": "1.6.19", + "@nocobase/plugin-workflow-response-message": "1.6.19", "@nocobase/plugin-workflow-sql": "1.6.19", "@nocobase/server": "1.6.19", "cronstrue": "^2.11.0",