mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
feat: make commercial plugins free (#6663)
* feat: make commercial plugins free * fix: rm pkg * fix: deps * fix: deps * fix: deps * fix: test error * fix: test error * fix: test error
This commit is contained in:
parent
7c1ccc73f6
commit
1db56657e8
@ -460,8 +460,16 @@ exports.initEnv = function initEnv() {
|
|||||||
process.env.SOCKET_PATH = generateGatewayPath();
|
process.env.SOCKET_PATH = generateGatewayPath();
|
||||||
fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true });
|
fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true });
|
||||||
fs.mkdirpSync(process.env.PM2_HOME, { 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');
|
const pkgs = [
|
||||||
fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true });
|
'@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 () {
|
exports.generatePlugins = function () {
|
||||||
|
2
packages/plugins/@nocobase/plugin-departments/.npmignore
Normal file
2
packages/plugins/@nocobase/plugin-departments/.npmignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
1
packages/plugins/@nocobase/plugin-departments/README.md
Normal file
1
packages/plugins/@nocobase/plugin-departments/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# @nocobase/plugin-department
|
2
packages/plugins/@nocobase/plugin-departments/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-departments/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/client';
|
||||||
|
export { default } from './dist/client';
|
1
packages/plugins/@nocobase/plugin-departments/client.js
Normal file
1
packages/plugins/@nocobase/plugin-departments/client.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client/index.js');
|
22
packages/plugins/@nocobase/plugin-departments/package.json
Normal file
22
packages/plugins/@nocobase/plugin-departments/package.json
Normal file
@ -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"
|
||||||
|
}
|
2
packages/plugins/@nocobase/plugin-departments/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-departments/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/server';
|
||||||
|
export { default } from './dist/server';
|
1
packages/plugins/@nocobase/plugin-departments/server.js
Normal file
1
packages/plugins/@nocobase/plugin-departments/server.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/server/index.js');
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ResourcesContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
setUser,
|
||||||
|
department,
|
||||||
|
setDepartment,
|
||||||
|
usersResource: { service: userService },
|
||||||
|
departmentsResource: { service: departmentService },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</ResourcesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DepartmentsListProvider: React.FC = (props) => {
|
||||||
|
const { departmentsResource } = useContext(ResourcesContext);
|
||||||
|
const { service } = departmentsResource || {};
|
||||||
|
return (
|
||||||
|
<ResourceActionContext.Provider value={{ ...service }}>
|
||||||
|
<CollectionProvider_deprecated collection={departmentCollection}>{props.children}</CollectionProvider_deprecated>
|
||||||
|
</ResourceActionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UsersListProvider: React.FC = (props) => {
|
||||||
|
const { usersResource } = useContext(ResourcesContext);
|
||||||
|
const { service } = usersResource || {};
|
||||||
|
const form = useMemo(() => createForm(), []);
|
||||||
|
const field = form.createField({ name: 'table' });
|
||||||
|
return (
|
||||||
|
<FormContext.Provider value={form}>
|
||||||
|
<TableBlockContext.Provider value={{ service, field }}>
|
||||||
|
<CollectionProvider_deprecated collection={userCollection}>{props.children}</CollectionProvider_deprecated>
|
||||||
|
</TableBlockContext.Provider>
|
||||||
|
</FormContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SchemaSettings } from '@nocobase/client';
|
||||||
|
import { enableLink, fieldComponent, titleField } from './fieldSettings';
|
||||||
|
|
||||||
|
export const DepartmentOwnersFieldSettings = new SchemaSettings({
|
||||||
|
name: 'fieldSettings:component:DepartmentOwnersField',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
...fieldComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...titleField,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...enableLink,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <div style={{ color: '#ccc' }}>{t('This field is currently not supported for use in form blocks.')} </div>;
|
||||||
|
}, mapReadPretty(AssociationField.ReadPretty));
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SchemaSettings } from '@nocobase/client';
|
||||||
|
import { enableLink, fieldComponent, titleField } from './fieldSettings';
|
||||||
|
|
||||||
|
export const UserDepartmentsFieldSettings = new SchemaSettings({
|
||||||
|
name: 'fieldSettings:component:UserDepartmentsField',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
...fieldComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...titleField,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...enableLink,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SchemaSettings } from '@nocobase/client';
|
||||||
|
import { enableLink, fieldComponent, titleField } from './fieldSettings';
|
||||||
|
|
||||||
|
export const UserMainDepartmentFieldSettings = new SchemaSettings({
|
||||||
|
name: 'fieldSettings:component:UserMainDepartmentField',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
...fieldComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...titleField,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...enableLink,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Field>();
|
||||||
|
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<Field>();
|
||||||
|
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<Field>();
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './ReadOnlyAssociationField';
|
||||||
|
export * from './UserDepartmentsField';
|
||||||
|
export * from './UserMainDepartmentField';
|
||||||
|
export * from './DepartmentOwnersField';
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
style={{ padding: '0 8px' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
setOpen(true);
|
||||||
|
run({
|
||||||
|
values: { keyword, limit, ...props },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Load more')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItems = () => {
|
||||||
|
const items: MenuProps['items'] = [];
|
||||||
|
if (!users.length && !departments.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: '0',
|
||||||
|
label: <Empty description={t('No results')} image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||||
|
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: (
|
||||||
|
<div onClick={() => setUser(user)}>
|
||||||
|
<div>{user.nickname || user.username}</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: token.fontSizeSM,
|
||||||
|
color: token.colorTextDescription,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${user.username}${user.phone ? ' | ' + user.phone : ''}${user.email ? ' | ' + user.email : ''}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
if (moreUsers) {
|
||||||
|
items.push({
|
||||||
|
type: 'group',
|
||||||
|
key: '0-loadMore',
|
||||||
|
label: <LoadMore type="user" last={users[users.length - 1].id} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (departments.length) {
|
||||||
|
items.push({
|
||||||
|
key: '1',
|
||||||
|
type: 'group',
|
||||||
|
label: t('Departments'),
|
||||||
|
children: departments.map((department: any) => ({
|
||||||
|
key: department.id,
|
||||||
|
label: <div onClick={() => setDepartment(department)}>{getTitle(department)}</div>,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
if (moreDepartments) {
|
||||||
|
items.push({
|
||||||
|
type: 'group',
|
||||||
|
key: '1-loadMore',
|
||||||
|
label: <LoadMore type="department" last={departments[departments.length - 1].id} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
menu={{ items: getItems() }}
|
||||||
|
overlayClassName={styles.searchDropdown}
|
||||||
|
trigger={['click']}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => setOpen(open)}
|
||||||
|
>
|
||||||
|
<Input.Search
|
||||||
|
allowClear
|
||||||
|
onClick={() => {
|
||||||
|
if (!keyword) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={() => setDepartment(null)}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={t('Search for departments, users')}
|
||||||
|
style={{ marginBottom: '20px' }}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<typeof useDepartmentManager>);
|
||||||
|
|
||||||
|
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 }) => <DepartmentTree.Item node={node} setVisible={setVisible} setDrawer={setDrawer} />,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaComponentOptions scope={{ useCreateDepartment, useUpdateDepartment }}>
|
||||||
|
<DepartmentTreeContext.Provider value={departmentManager}>
|
||||||
|
<Row>
|
||||||
|
<AggregateSearch />
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
style={{
|
||||||
|
textAlign: 'left',
|
||||||
|
marginBottom: '5px',
|
||||||
|
background: department ? '' : token.colorBgTextHover,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setDepartment(null);
|
||||||
|
}}
|
||||||
|
block
|
||||||
|
>
|
||||||
|
{t('All users')}
|
||||||
|
</Button>
|
||||||
|
<NewDepartment />
|
||||||
|
</Row>
|
||||||
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
|
<DepartmentTree />
|
||||||
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
|
<RecordProvider record={drawer.node || {}}>
|
||||||
|
<SchemaComponent scope={{ t }} components={{ DepartmentOwnersField }} schema={drawer.schema || {}} />
|
||||||
|
</RecordProvider>
|
||||||
|
</ActionContextProvider>
|
||||||
|
</DepartmentTreeContext.Provider>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
import { DepartmentManagement } from './DepartmentManagement';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
|
||||||
|
export const DepartmentBlock: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ DepartmentManagement }}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-component': 'DepartmentManagement',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Field>();
|
||||||
|
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) => (
|
||||||
|
<span key={index}>
|
||||||
|
<a
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDepartment(deptsMap[dept.id]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getDepartmentTitle(dept)}
|
||||||
|
</a>
|
||||||
|
{index !== values.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : ''}
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
return <EllipsisWithTooltip ellipsis={true}>{depts}</EllipsisWithTooltip>;
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<SchemaComponentOptions components={{ SuperiorDepartmentSelect, DepartmentSelect }}>
|
||||||
|
<Row gutter={48} style={{ flexWrap: 'nowrap' }}>
|
||||||
|
<Col span={6} style={{ borderRight: '1px solid #eee', minWidth: '300px' }}>
|
||||||
|
<DepartmentsListProvider>
|
||||||
|
<Department />
|
||||||
|
</DepartmentsListProvider>
|
||||||
|
</Col>
|
||||||
|
<Col flex="auto" style={{ overflow: 'hidden' }}>
|
||||||
|
<UsersListProvider>
|
||||||
|
<Member />
|
||||||
|
</UsersListProvider>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Field>();
|
||||||
|
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) => (
|
||||||
|
<ResourceActionProvider
|
||||||
|
collection="users"
|
||||||
|
request={{
|
||||||
|
resource: `departments/${department.id}/members`,
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
filter: field.value?.length
|
||||||
|
? {
|
||||||
|
id: {
|
||||||
|
$notIn: field.value.map((owner: any) => owner.id),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</ResourceActionProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
|
<Select
|
||||||
|
open={false}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!value) {
|
||||||
|
field.setValue([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.setValue(
|
||||||
|
value.map(({ label, value }: { label: string; value: string }) => ({
|
||||||
|
id: value,
|
||||||
|
nickname: label,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
mode="multiple"
|
||||||
|
value={value}
|
||||||
|
labelInValue={true}
|
||||||
|
onDropdownVisibleChange={(open) => setVisible(open)}
|
||||||
|
/>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={departmentOwnersSchema}
|
||||||
|
components={{ RequestProvider }}
|
||||||
|
scope={{ department, handleSelect, useSelectOwners }}
|
||||||
|
/>
|
||||||
|
</ActionContextProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
CollectionContext,
|
||||||
|
CollectionProvider_deprecated,
|
||||||
|
ResourceActionContext,
|
||||||
|
SchemaComponent,
|
||||||
|
mergeFilter,
|
||||||
|
removeNullCondition,
|
||||||
|
useFilterFieldOptions,
|
||||||
|
useFilterFieldProps,
|
||||||
|
useResourceActionContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import { useDepartmentManager } from '../hooks';
|
||||||
|
import { Table, TablePaginationConfig } from 'antd';
|
||||||
|
import { departmentCollection } from '../collections/departments';
|
||||||
|
import { useDepartmentTranslation } from '../locale';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { Field } from '@formily/core';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { getDepartmentTitle } from '../utils';
|
||||||
|
|
||||||
|
const ExpandMetaContext = createContext<any>({});
|
||||||
|
|
||||||
|
export const useFilterActionProps = () => {
|
||||||
|
const { setHasFilter, setExpandedKeys } = useContext(ExpandMetaContext);
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
const collection = useContext(CollectionContext);
|
||||||
|
const options = useFilterFieldOptions(collection.fields);
|
||||||
|
const service = useResourceActionContext();
|
||||||
|
const { run, defaultRequest } = service;
|
||||||
|
const field = useField<Field>();
|
||||||
|
const { params } = defaultRequest || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
onSubmit: async (values: any) => {
|
||||||
|
// filter parameter for the block
|
||||||
|
const defaultFilter = params.filter;
|
||||||
|
// filter parameter for the filter action
|
||||||
|
const filter = removeNullCondition(values?.filter);
|
||||||
|
run({
|
||||||
|
...params,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
filter: mergeFilter([filter, defaultFilter]),
|
||||||
|
});
|
||||||
|
const items = filter?.$and || filter?.$or;
|
||||||
|
if (items?.length) {
|
||||||
|
field.title = t('{{count}} filter items', { count: items?.length || 0 });
|
||||||
|
setHasFilter(true);
|
||||||
|
} else {
|
||||||
|
field.title = t('Filter');
|
||||||
|
setHasFilter(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onReset() {
|
||||||
|
run({
|
||||||
|
...(params || {}),
|
||||||
|
filter: {
|
||||||
|
...(params?.filter || {}),
|
||||||
|
parentId: null,
|
||||||
|
},
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
field.title = t('Filter');
|
||||||
|
setHasFilter(false);
|
||||||
|
setExpandedKeys([]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDefaultDisabled = () => {
|
||||||
|
return {
|
||||||
|
disabled: () => false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const InternalDepartmentTable: React.FC<{
|
||||||
|
useDisabled?: () => {
|
||||||
|
disabled: (record: any) => boolean;
|
||||||
|
};
|
||||||
|
}> = ({ useDisabled = useDefaultDisabled }) => {
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
const ctx = useResourceActionContext();
|
||||||
|
console.log(ctx);
|
||||||
|
const { run, data, loading, defaultRequest } = ctx;
|
||||||
|
const { resource, resourceOf, params } = defaultRequest || {};
|
||||||
|
const { treeData, initData, loadData } = useDepartmentManager({
|
||||||
|
resource,
|
||||||
|
resourceOf,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
const field = useField<Field>();
|
||||||
|
const { disabled } = useDisabled();
|
||||||
|
const { hasFilter, expandedKeys, setExpandedKeys } = useContext(ExpandMetaContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasFilter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initData(data?.data);
|
||||||
|
}, [data, initData, loading, hasFilter]);
|
||||||
|
|
||||||
|
const pagination: TablePaginationConfig = {};
|
||||||
|
if (params?.pageSize) {
|
||||||
|
pagination.defaultPageSize = params.pageSize;
|
||||||
|
}
|
||||||
|
if (!pagination.total && data?.meta) {
|
||||||
|
const { count, page, pageSize } = data.meta;
|
||||||
|
pagination.total = count;
|
||||||
|
pagination.current = page;
|
||||||
|
pagination.pageSize = pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
dataIndex: 'title',
|
||||||
|
title: t('Department name'),
|
||||||
|
render: (text, record) => (hasFilter ? getDepartmentTitle(record) : text),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys: (field?.value || []).map((dept: any) => dept.id),
|
||||||
|
onChange: (keys, depts) => field?.setValue?.(depts),
|
||||||
|
getCheckboxProps: (record: any) => ({
|
||||||
|
disabled: disabled(record),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
pagination={{
|
||||||
|
showSizeChanger: true,
|
||||||
|
...pagination,
|
||||||
|
onChange(page, pageSize) {
|
||||||
|
run({
|
||||||
|
...(ctx?.params?.[0] || {}),
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
dataSource={hasFilter ? data?.data || [] : treeData}
|
||||||
|
expandable={{
|
||||||
|
onExpand: (expanded, record) => {
|
||||||
|
loadData({
|
||||||
|
key: record.id,
|
||||||
|
children: record.children,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
expandedRowKeys: expandedKeys,
|
||||||
|
onExpandedRowsChange: (keys) => setExpandedKeys(keys),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RequestProvider: React.FC<{
|
||||||
|
useDataSource: any;
|
||||||
|
}> = (props) => {
|
||||||
|
const [expandedKeys, setExpandedKeys] = useState([]);
|
||||||
|
const [hasFilter, setHasFilter] = useState(false);
|
||||||
|
const { useDataSource } = props;
|
||||||
|
const service = useDataSource({
|
||||||
|
manual: true,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
service.run({
|
||||||
|
filter: {
|
||||||
|
parentId: null,
|
||||||
|
},
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<ResourceActionContext.Provider value={{ ...service }}>
|
||||||
|
<CollectionProvider_deprecated collection={departmentCollection}>
|
||||||
|
<ExpandMetaContext.Provider value={{ expandedKeys, setExpandedKeys, hasFilter, setHasFilter }}>
|
||||||
|
{props.children}
|
||||||
|
</ExpandMetaContext.Provider>
|
||||||
|
</CollectionProvider_deprecated>
|
||||||
|
</ResourceActionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DepartmentTable: React.FC<{
|
||||||
|
useDataSource: any;
|
||||||
|
useDisabled?: (record: any) => boolean;
|
||||||
|
}> = ({ useDataSource, useDisabled }) => {
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
scope={{ useDisabled, useFilterActionProps }}
|
||||||
|
components={{ InternalDepartmentTable, RequestProvider }}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'RequestProvider',
|
||||||
|
'x-component-props': {
|
||||||
|
useDataSource,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Filter") }}',
|
||||||
|
default: {
|
||||||
|
$and: [{ title: { $includes: '' } }],
|
||||||
|
},
|
||||||
|
'x-action': 'filter',
|
||||||
|
'x-component': 'Filter.Action',
|
||||||
|
'x-use-component-props': 'useFilterActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'FilterOutlined',
|
||||||
|
},
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
departments: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'InternalDepartmentTable',
|
||||||
|
'x-component-props': {
|
||||||
|
useDisabled: '{{ useDisabled }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
import { Tree, Dropdown, App, Empty } from 'antd';
|
||||||
|
import { MoreOutlined } from '@ant-design/icons';
|
||||||
|
import { useAPIClient, useResourceActionContext } from '@nocobase/client';
|
||||||
|
import { useDepartmentTranslation } from '../locale';
|
||||||
|
import { editDepartmentSchema, newSubDepartmentSchema } from './schemas/departments';
|
||||||
|
import { ResourcesContext } from '../ResourcesProvider';
|
||||||
|
import { DepartmentTreeContext } from './Department';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
type DepartmentTreeProps = {
|
||||||
|
node: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
parent?: any;
|
||||||
|
};
|
||||||
|
setVisible: (visible: boolean) => void;
|
||||||
|
setDrawer: (schema: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DepartmentTree: React.FC & {
|
||||||
|
Item: React.FC<DepartmentTreeProps>;
|
||||||
|
} = () => {
|
||||||
|
const { data, loading } = useResourceActionContext();
|
||||||
|
const { department, setDepartment, setUser } = useContext(ResourcesContext);
|
||||||
|
const { treeData, nodeMap, loadData, loadedKeys, setLoadedKeys, initData, expandedKeys, setExpandedKeys } =
|
||||||
|
useContext(DepartmentTreeContext);
|
||||||
|
const handleSelect = (keys: number[]) => {
|
||||||
|
if (!keys.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node = nodeMap[keys[0]];
|
||||||
|
setDepartment(node);
|
||||||
|
setUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpand = (keys: number[]) => {
|
||||||
|
setExpandedKeys(keys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoad = (keys: number[]) => {
|
||||||
|
setLoadedKeys(keys);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData(data?.data);
|
||||||
|
}, [data, initData, loading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!department) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const getIds = (node: any) => {
|
||||||
|
if (node.parent) {
|
||||||
|
return [node.parent.id, ...getIds(node.parent)];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
const newKeys = getIds(department);
|
||||||
|
setExpandedKeys((keys) => Array.from(new Set([...keys, ...newKeys])));
|
||||||
|
}, [department, setExpandedKeys]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
height: 65vh;
|
||||||
|
overflow: auto;
|
||||||
|
.ant-tree-node-content-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{treeData?.length ? (
|
||||||
|
<Tree.DirectoryTree
|
||||||
|
loadData={loadData}
|
||||||
|
treeData={treeData}
|
||||||
|
loadedKeys={loadedKeys}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
selectedKeys={[department?.id]}
|
||||||
|
onExpand={handleExpand}
|
||||||
|
onLoad={handleLoad}
|
||||||
|
expandedKeys={expandedKeys}
|
||||||
|
expandAction={false}
|
||||||
|
showIcon={false}
|
||||||
|
fieldNames={{
|
||||||
|
key: 'id',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DepartmentTree.Item = function DepartmentTreeItem({ node, setVisible, setDrawer }: DepartmentTreeProps) {
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
const { refreshAsync } = useResourceActionContext();
|
||||||
|
const { setLoadedKeys, expandedKeys, setExpandedKeys } = useContext(DepartmentTreeContext);
|
||||||
|
const { modal, message } = App.useApp();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const deleteDepartment = () => {
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Delete'),
|
||||||
|
content: t('Are you sure you want to delete it?'),
|
||||||
|
onOk: async () => {
|
||||||
|
await api.resource('departments').destroy({ filterByTk: node.id });
|
||||||
|
message.success(t('Deleted successfully'));
|
||||||
|
setExpandedKeys((keys) => keys.filter((k) => k !== node.id));
|
||||||
|
const expanded = [...expandedKeys];
|
||||||
|
setLoadedKeys([]);
|
||||||
|
setExpandedKeys([]);
|
||||||
|
await refreshAsync();
|
||||||
|
setExpandedKeys(expanded);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const openDrawer = (schema: any) => {
|
||||||
|
setDrawer({ schema, node });
|
||||||
|
setVisible(true);
|
||||||
|
};
|
||||||
|
const handleClick = ({ key, domEvent }) => {
|
||||||
|
domEvent.stopPropagation();
|
||||||
|
switch (key) {
|
||||||
|
case 'new-sub':
|
||||||
|
openDrawer(newSubDepartmentSchema);
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
openDrawer(editDepartmentSchema);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
deleteDepartment();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', overflow: 'hidden' }}>
|
||||||
|
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.title}</div>
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: t('New sub department'),
|
||||||
|
key: 'new-sub',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Edit department'),
|
||||||
|
key: 'edit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Delete department'),
|
||||||
|
key: 'delete',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onClick: handleClick,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginLeft: '15px' }}>
|
||||||
|
<MoreOutlined />
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useContext, useEffect } from 'react';
|
||||||
|
import { TreeSelect } from 'antd';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { Field } from '@formily/core';
|
||||||
|
import { useRecord } from '@nocobase/client';
|
||||||
|
import { ResourcesContext } from '../ResourcesProvider';
|
||||||
|
import { useDepartmentManager } from '../hooks/departments-manager';
|
||||||
|
|
||||||
|
export const DepartmentTreeSelect: React.FC<{
|
||||||
|
originData: any;
|
||||||
|
treeData: any[];
|
||||||
|
[key: string]: any;
|
||||||
|
}> = (props) => {
|
||||||
|
const field = useField<Field>();
|
||||||
|
const [value, setValue] = React.useState({ label: null, value: null });
|
||||||
|
const { treeData, initData, getByKeyword, loadData, loadedKeys, setLoadedKeys, originData } = props;
|
||||||
|
|
||||||
|
const handleSearch = async (keyword: string) => {
|
||||||
|
if (!keyword) {
|
||||||
|
initData(originData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await getByKeyword(keyword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitle = useCallback((record: any) => {
|
||||||
|
const title = record.title;
|
||||||
|
const parent = record.parent;
|
||||||
|
if (parent) {
|
||||||
|
return getTitle(parent) + ' / ' + title;
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData(originData);
|
||||||
|
}, [originData, initData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!field.value) {
|
||||||
|
setValue({ label: null, value: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue({
|
||||||
|
label: getTitle(field.value) || field.value.label,
|
||||||
|
value: field.value.id,
|
||||||
|
});
|
||||||
|
}, [field.value, getTitle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeSelect
|
||||||
|
value={value}
|
||||||
|
onSelect={(_: any, node: any) => {
|
||||||
|
field.setValue(node);
|
||||||
|
}}
|
||||||
|
onChange={(value: any) => {
|
||||||
|
if (!value) {
|
||||||
|
field.setValue(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
treeData={treeData}
|
||||||
|
treeLoadedKeys={loadedKeys}
|
||||||
|
onTreeLoad={(keys: any[]) => setLoadedKeys(keys)}
|
||||||
|
loadData={(node: any) => loadData({ key: node.id, children: node.children })}
|
||||||
|
fieldNames={{
|
||||||
|
value: 'id',
|
||||||
|
}}
|
||||||
|
showSearch
|
||||||
|
allowClear
|
||||||
|
treeNodeFilterProp="title"
|
||||||
|
onSearch={handleSearch}
|
||||||
|
labelInValue={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DepartmentSelect: React.FC = () => {
|
||||||
|
const departmentManager = useDepartmentManager();
|
||||||
|
const { departmentsResource } = useContext(ResourcesContext);
|
||||||
|
const {
|
||||||
|
service: { data },
|
||||||
|
} = departmentsResource || {};
|
||||||
|
return <DepartmentTreeSelect {...departmentManager} originData={data?.data} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SuperiorDepartmentSelect: React.FC = () => {
|
||||||
|
const departmentManager = useDepartmentManager();
|
||||||
|
const { setTreeData, getChildrenIds } = departmentManager;
|
||||||
|
const record = useRecord() as any;
|
||||||
|
const { departmentsResource } = useContext(ResourcesContext);
|
||||||
|
const {
|
||||||
|
service: { data },
|
||||||
|
} = departmentsResource || {};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!record.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const childrenIds = getChildrenIds(record.id);
|
||||||
|
childrenIds.push(record.id);
|
||||||
|
setTreeData((treeData) => {
|
||||||
|
const setDisabled = (treeData: any[]) => {
|
||||||
|
return treeData.map((node) => {
|
||||||
|
if (childrenIds.includes(node.id)) {
|
||||||
|
node.disabled = true;
|
||||||
|
}
|
||||||
|
if (node.children) {
|
||||||
|
node.children = setDisabled(node.children);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return setDisabled(treeData);
|
||||||
|
});
|
||||||
|
}, [setTreeData, record.id, getChildrenIds]);
|
||||||
|
|
||||||
|
return <DepartmentTreeSelect {...departmentManager} originData={data?.data} />;
|
||||||
|
};
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useDepartmentTranslation } from '../locale';
|
||||||
|
import { Checkbox, useRecord } from '@nocobase/client';
|
||||||
|
import { ResourcesContext } from '../ResourcesProvider';
|
||||||
|
|
||||||
|
export const IsOwnerField: React.FC = () => {
|
||||||
|
const { department } = useContext(ResourcesContext);
|
||||||
|
const record = useRecord() as any;
|
||||||
|
const dept = (record.departments || []).find((dept: any) => dept?.id === department?.id);
|
||||||
|
|
||||||
|
return <Checkbox.ReadPretty value={dept?.departmentsUsers.isOwner} />;
|
||||||
|
};
|
@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useContext, useRef, useEffect, useMemo } from 'react';
|
||||||
|
import { useDepartmentTranslation } from '../locale';
|
||||||
|
import {
|
||||||
|
CollectionContext,
|
||||||
|
ResourceActionProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
useAPIClient,
|
||||||
|
useActionContext,
|
||||||
|
useFilterFieldOptions,
|
||||||
|
useFilterFieldProps,
|
||||||
|
useRecord,
|
||||||
|
useResourceActionContext,
|
||||||
|
useTableBlockContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { membersActionSchema, addMembersSchema, rowRemoveActionSchema, getMembersSchema } from './schemas/users';
|
||||||
|
import { App } from 'antd';
|
||||||
|
import { DepartmentField } from './DepartmentField';
|
||||||
|
import { IsOwnerField } from './IsOwnerField';
|
||||||
|
import { UserDepartmentsField } from './UserDepartmentsField';
|
||||||
|
import { ResourcesContext } from '../ResourcesProvider';
|
||||||
|
import { useTableBlockProps } from '../hooks/useTableBlockProps';
|
||||||
|
|
||||||
|
const AddMembersListProvider: React.FC = (props) => {
|
||||||
|
const { department } = useContext(ResourcesContext);
|
||||||
|
return (
|
||||||
|
<ResourceActionProvider
|
||||||
|
{...{
|
||||||
|
collection: 'users',
|
||||||
|
request: {
|
||||||
|
resource: 'users',
|
||||||
|
action: 'listExcludeDept',
|
||||||
|
params: {
|
||||||
|
departmentId: department?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</ResourceActionProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAddMembersFilterActionProps = () => {
|
||||||
|
const collection = useContext(CollectionContext);
|
||||||
|
const options = useFilterFieldOptions(collection.fields);
|
||||||
|
const service = useResourceActionContext();
|
||||||
|
return useFilterFieldProps({
|
||||||
|
options,
|
||||||
|
params: service.state?.params?.[0] || service.params,
|
||||||
|
service,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddMembers: React.FC = () => {
|
||||||
|
const { department } = useContext(ResourcesContext);
|
||||||
|
// This resource is the list of members of the current department.
|
||||||
|
const {
|
||||||
|
service: { refresh },
|
||||||
|
} = useTableBlockContext();
|
||||||
|
const selectedKeys = useRef([]);
|
||||||
|
const api = useAPIClient();
|
||||||
|
|
||||||
|
const useAddMembersActionProps = () => {
|
||||||
|
const { department } = useContext(ResourcesContext);
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
const selected = selectedKeys.current;
|
||||||
|
if (!selected?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.resource('departments.members', department.id).add({
|
||||||
|
values: selected,
|
||||||
|
});
|
||||||
|
selectedKeys.current = [];
|
||||||
|
refresh();
|
||||||
|
setVisible?.(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (keys: any[]) => {
|
||||||
|
selectedKeys.current = keys;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
scope={{
|
||||||
|
useAddMembersActionProps,
|
||||||
|
department,
|
||||||
|
handleSelect,
|
||||||
|
useAddMembersFilterActionProps,
|
||||||
|
}}
|
||||||
|
components={{ AddMembersListProvider }}
|
||||||
|
schema={addMembersSchema}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useBulkRemoveMembersAction = () => {
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const {
|
||||||
|
service: { refresh },
|
||||||
|
field,
|
||||||
|
} = useTableBlockContext();
|
||||||
|
const { department } = useContext(ResourcesContext);
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
const selected = field?.data?.selectedRowKeys;
|
||||||
|
if (!selected?.length) {
|
||||||
|
message.warning(t('Please select members'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.resource('departments.members', department.id).remove({
|
||||||
|
values: selected,
|
||||||
|
});
|
||||||
|
field.data.selectedRowKeys = [];
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRemoveMemberAction = () => {
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { department } = useContext(ResourcesContext);
|
||||||
|
const { id } = useRecord() as any;
|
||||||
|
const {
|
||||||
|
service: { refresh },
|
||||||
|
} = useTableBlockContext();
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
await api.resource('departments.members', department.id).remove({
|
||||||
|
values: [id],
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useShowTotal = () => {
|
||||||
|
const {
|
||||||
|
service: { data },
|
||||||
|
} = useTableBlockContext();
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
return t('Total {{count}} members', { count: data?.meta?.count });
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRefreshActionProps = () => {
|
||||||
|
const { service } = useTableBlockContext();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
service?.refresh?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const RowRemoveAction = () => {
|
||||||
|
const { department } = useContext(ResourcesContext);
|
||||||
|
return department ? <SchemaComponent scope={{ useRemoveMemberAction }} schema={rowRemoveActionSchema} /> : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MemberActions = () => {
|
||||||
|
const { department } = useContext(ResourcesContext);
|
||||||
|
return department ? <SchemaComponent schema={membersActionSchema} /> : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useMemberFilterActionProps = () => {
|
||||||
|
const collection = useContext(CollectionContext);
|
||||||
|
const options = useFilterFieldOptions(collection.fields);
|
||||||
|
const { service } = useTableBlockContext();
|
||||||
|
return useFilterFieldProps({
|
||||||
|
options,
|
||||||
|
params: service.state?.params?.[0] || service.params,
|
||||||
|
service,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Member: React.FC = () => {
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
const { department, user } = useContext(ResourcesContext);
|
||||||
|
const {
|
||||||
|
service: { data, setState },
|
||||||
|
} = useTableBlockContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState?.({ selectedRowKeys: [] });
|
||||||
|
}, [data, setState]);
|
||||||
|
|
||||||
|
const schema = useMemo(() => getMembersSchema(department, user), [department, user]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!user ? <h2>{t(department?.title || 'All users')}</h2> : <h2>{t('Search results')}</h2>}
|
||||||
|
<SchemaComponent
|
||||||
|
scope={{
|
||||||
|
useBulkRemoveMembersAction,
|
||||||
|
t,
|
||||||
|
useShowTotal,
|
||||||
|
useRefreshActionProps,
|
||||||
|
useMemberFilterActionProps,
|
||||||
|
useTableBlockProps,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
MemberActions,
|
||||||
|
AddMembers,
|
||||||
|
RowRemoveAction,
|
||||||
|
DepartmentField,
|
||||||
|
IsOwnerField,
|
||||||
|
UserDepartmentsField,
|
||||||
|
}}
|
||||||
|
schema={schema}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useDepartmentTranslation } from '../locale';
|
||||||
|
|
||||||
|
export const NewDepartment: React.FC = () => {
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
scope={{ t }}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
newDepartment: {
|
||||||
|
type: 'void',
|
||||||
|
title: t('New department'),
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'text',
|
||||||
|
icon: 'PlusOutlined',
|
||||||
|
style: {
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-decorator': 'Form',
|
||||||
|
title: t('New department'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
parent: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field': 'departments.parent',
|
||||||
|
'x-component-props': {
|
||||||
|
component: 'DepartmentSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field': 'departments.roles',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
cancel: {
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useCreateDepartment }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,273 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActionContextProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
useAPIClient,
|
||||||
|
useRecord,
|
||||||
|
useRequest,
|
||||||
|
useResourceActionContext,
|
||||||
|
useTableBlockContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Tag, Button, Dropdown, App } from 'antd';
|
||||||
|
import { PlusOutlined, MoreOutlined } from '@ant-design/icons';
|
||||||
|
import { Field } from '@formily/core';
|
||||||
|
import { useField, useForm } from '@formily/react';
|
||||||
|
import { userDepartmentsSchema } from './schemas/users';
|
||||||
|
import { getDepartmentTitle } from '../utils';
|
||||||
|
import { useDepartmentTranslation } from '../locale';
|
||||||
|
import { DepartmentTable } from './DepartmentTable';
|
||||||
|
|
||||||
|
const useDataSource = (options?: any) => {
|
||||||
|
const defaultRequest = {
|
||||||
|
resource: 'departments',
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
appends: ['parent(recursively=true)'],
|
||||||
|
// filter: {
|
||||||
|
// parentId: null,
|
||||||
|
// },
|
||||||
|
sort: ['createdAt'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const service = useRequest(defaultRequest, options);
|
||||||
|
return {
|
||||||
|
...service,
|
||||||
|
defaultRequest,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserDepartmentsField: React.FC = () => {
|
||||||
|
const { modal, message } = App.useApp();
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const user = useRecord() as any;
|
||||||
|
const field = useField<Field>();
|
||||||
|
const {
|
||||||
|
service: { refresh },
|
||||||
|
} = useTableBlockContext();
|
||||||
|
|
||||||
|
const formatData = (data: any[]) => {
|
||||||
|
if (!data?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.map((department) => ({
|
||||||
|
...department,
|
||||||
|
isMain: department.departmentsUsers?.isMain,
|
||||||
|
isOwner: department.departmentsUsers?.isOwner,
|
||||||
|
title: getDepartmentTitle(department),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = useAPIClient();
|
||||||
|
useRequest(
|
||||||
|
() =>
|
||||||
|
api
|
||||||
|
.resource(`users.departments`, user.id)
|
||||||
|
.list({
|
||||||
|
appends: ['parent(recursively=true)'],
|
||||||
|
paginate: false,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const data = formatData(res?.data?.data);
|
||||||
|
field.setValue(data);
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
ready: user.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const useAddDepartments = () => {
|
||||||
|
const api = useAPIClient();
|
||||||
|
const drawerForm = useForm();
|
||||||
|
const { departments } = drawerForm.values || {};
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
await api.resource('users.departments', user.id).add({
|
||||||
|
values: departments.map((dept: any) => dept.id),
|
||||||
|
});
|
||||||
|
drawerForm.reset();
|
||||||
|
field.setValue([
|
||||||
|
...field.value,
|
||||||
|
...departments.map((dept: any, index: number) => ({
|
||||||
|
...dept,
|
||||||
|
isMain: index === 0 && field.value.length === 0,
|
||||||
|
title: getDepartmentTitle(dept),
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
setVisible(false);
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDepartment = (dept: any) => {
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Remove department'),
|
||||||
|
content: t('Are you sure you want to remove it?'),
|
||||||
|
onOk: async () => {
|
||||||
|
await api.resource('users.departments', user.id).remove({ values: [dept.id] });
|
||||||
|
message.success(t('Deleted successfully'));
|
||||||
|
field.setValue(
|
||||||
|
field.value
|
||||||
|
.filter((d: any) => d.id !== dept.id)
|
||||||
|
.map((d: any, index: number) => ({
|
||||||
|
...d,
|
||||||
|
isMain: (dept.isMain && index === 0) || d.isMain,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMainDepartment = async (dept: any) => {
|
||||||
|
await api.resource('users').setMainDepartment({
|
||||||
|
values: {
|
||||||
|
userId: user.id,
|
||||||
|
departmentId: dept.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
message.success(t('Set successfully'));
|
||||||
|
field.setValue(
|
||||||
|
field.value.map((d: any) => ({
|
||||||
|
...d,
|
||||||
|
isMain: d.id === dept.id,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setOwner = async (dept: any) => {
|
||||||
|
await api.resource('departments').setOwner({
|
||||||
|
values: {
|
||||||
|
userId: user.id,
|
||||||
|
departmentId: dept.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
message.success(t('Set successfully'));
|
||||||
|
field.setValue(
|
||||||
|
field.value.map((d: any) => ({
|
||||||
|
...d,
|
||||||
|
isOwner: d.id === dept.id ? true : d.isOwner,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeOwner = async (dept: any) => {
|
||||||
|
await api.resource('departments').removeOwner({
|
||||||
|
values: {
|
||||||
|
userId: user.id,
|
||||||
|
departmentId: dept.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
message.success(t('Set successfully'));
|
||||||
|
field.setValue(
|
||||||
|
field.value.map((d: any) => ({
|
||||||
|
...d,
|
||||||
|
isOwner: d.id === dept.id ? false : d.isOwner,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (key: string, dept: any) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'setMain':
|
||||||
|
setMainDepartment(dept);
|
||||||
|
break;
|
||||||
|
case 'setOwner':
|
||||||
|
setOwner(dept);
|
||||||
|
break;
|
||||||
|
case 'removeOwner':
|
||||||
|
removeOwner(dept);
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
removeDepartment(dept);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDisabled = () => ({
|
||||||
|
disabled: (record: any) => {
|
||||||
|
return field.value.some((dept: any) => dept.id === record.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
|
<>
|
||||||
|
{(field?.value || []).map((dept) => (
|
||||||
|
<Tag style={{ padding: '5px 8px', background: 'transparent', marginBottom: '5px' }} key={dept.id}>
|
||||||
|
<span style={{ marginRight: '5px' }}>{dept.title}</span>
|
||||||
|
{dept.isMain ? (
|
||||||
|
<Tag color="processing" bordered={false}>
|
||||||
|
{t('Main')}
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
{/* {dept.isOwner ? ( */}
|
||||||
|
{/* <Tag color="gold" bordered={false}> */}
|
||||||
|
{/* {t('Owner')} */}
|
||||||
|
{/* </Tag> */}
|
||||||
|
{/* ) : ( */}
|
||||||
|
{/* '' */}
|
||||||
|
{/* )} */}
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
...(dept.isMain
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: t('Set as main department'),
|
||||||
|
key: 'setMain',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
// {
|
||||||
|
// label: dept.isOwner ? t('Remove owner role') : t('Set as owner'),
|
||||||
|
// key: dept.isOwner ? 'removeOwner' : 'setOwner',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
label: t('Remove'),
|
||||||
|
key: 'remove',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onClick: ({ key }) => handleClick(key, dept),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ float: 'right' }}>
|
||||||
|
<MoreOutlined />
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
<Button icon={<PlusOutlined />} onClick={() => setVisible(true)} />
|
||||||
|
</>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={userDepartmentsSchema}
|
||||||
|
components={{ DepartmentTable }}
|
||||||
|
scope={{ user, useDataSource, useAddDepartments, useDisabled }}
|
||||||
|
/>
|
||||||
|
</ActionContextProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,291 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { useAPIClient, useActionContext, useRecord, useRequest } from '@nocobase/client';
|
||||||
|
|
||||||
|
export const newSubDepartmentSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-decorator': 'Form',
|
||||||
|
'x-decorator-props': {
|
||||||
|
useValues(options: any) {
|
||||||
|
const ctx = useActionContext();
|
||||||
|
const record = useRecord();
|
||||||
|
return useRequest(() => Promise.resolve({ data: { parent: { ...record } } }), {
|
||||||
|
...options,
|
||||||
|
refreshDeps: [ctx.visible],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: '{{t("New sub department")}}',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
parent: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field': 'departments.parent',
|
||||||
|
'x-component-props': {
|
||||||
|
component: 'DepartmentSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field': 'departments.roles',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
cancel: {
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useCreateDepartment }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editDepartmentSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-decorator': 'Form',
|
||||||
|
'x-decorator-props': {
|
||||||
|
useValues(options: any) {
|
||||||
|
const api = useAPIClient();
|
||||||
|
const ctx = useActionContext();
|
||||||
|
const record = useRecord();
|
||||||
|
const result = useRequest(
|
||||||
|
() =>
|
||||||
|
api
|
||||||
|
.resource('departments')
|
||||||
|
.get({
|
||||||
|
filterByTk: record['id'],
|
||||||
|
appends: ['parent(recursively=true)', 'roles', 'owners'],
|
||||||
|
})
|
||||||
|
.then((res: any) => res?.data),
|
||||||
|
{ ...options, manual: true },
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (ctx.visible) {
|
||||||
|
result.run();
|
||||||
|
}
|
||||||
|
}, [ctx.visible]);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: '{{t("Edit department")}}',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
parent: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field': 'departments.parent',
|
||||||
|
'x-component-props': {
|
||||||
|
component: 'SuperiorDepartmentSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field': 'departments.roles',
|
||||||
|
},
|
||||||
|
owners: {
|
||||||
|
title: '{{t("Owners")}}',
|
||||||
|
'x-component': 'DepartmentOwnersField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
cancel: {
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useUpdateDepartment }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const departmentOwnersSchema = {
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
title: '{{t("Select Owners")}}',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'RequestProvider',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Filter") }}',
|
||||||
|
default: {
|
||||||
|
$and: [{ username: { $includes: '' } }, { nickname: { $includes: '' } }],
|
||||||
|
},
|
||||||
|
'x-action': 'filter',
|
||||||
|
'x-component': 'Filter.Action',
|
||||||
|
'x-use-component-props': 'useFilterActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'FilterOutlined',
|
||||||
|
},
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Table.Void',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
onChange: '{{ handleSelect }}',
|
||||||
|
},
|
||||||
|
useDataSource: '{{ cm.useDataSourceFromRAC }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nickname: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
nickname: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
cancel: {
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
title: '{{t("Confirm")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useSelectOwners }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,464 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
|
||||||
|
export const membersActionSchema = {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
properties: {
|
||||||
|
remove: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Remove")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'UserDeleteOutlined',
|
||||||
|
confirm: {
|
||||||
|
title: "{{t('Remove members')}}",
|
||||||
|
content: "{{t('Are you sure you want to remove these members?')}}",
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
useAction: '{{ useBulkRemoveMembersAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Add members")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'UserAddOutlined',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'AddMembers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rowRemoveActionSchema = {
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
remove: {
|
||||||
|
title: '{{ t("Remove") }}',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-component-props': {
|
||||||
|
confirm: {
|
||||||
|
title: "{{t('Remove member')}}",
|
||||||
|
content: "{{t('Are you sure you want to remove it?')}}",
|
||||||
|
},
|
||||||
|
useAction: '{{ useRemoveMemberAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMembersSchema = (department: any, user: any) => ({
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-component-props': {
|
||||||
|
heightMode: 'fullHeight',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
...(!user
|
||||||
|
? {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Filter") }}',
|
||||||
|
'x-action': 'filter',
|
||||||
|
'x-component': 'Filter.Action',
|
||||||
|
'x-use-component-props': 'useMemberFilterActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'FilterOutlined',
|
||||||
|
},
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
refresh: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Refresh") }}',
|
||||||
|
'x-action': 'refresh',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-use-component-props': 'useRefreshActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'ReloadOutlined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MemberActions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
table: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'TableV2',
|
||||||
|
'x-use-component-props': 'useTableBlockProps',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
showTotal: '{{ useShowTotal }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
nickname: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
nickname: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
departments: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
departments: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(department
|
||||||
|
? {
|
||||||
|
isOwner: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: '{{t("Owner")}}',
|
||||||
|
properties: {
|
||||||
|
isOwner: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-component': 'IsOwnerField',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
phone: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Actions")}}',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
'x-component-props': {
|
||||||
|
split: '|',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
update: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Configure")}}',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-decorator': 'FormV2',
|
||||||
|
title: '{{t("Configure")}}',
|
||||||
|
properties: {
|
||||||
|
departments: {
|
||||||
|
title: '{{t("Departments")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'UserDepartmentsField',
|
||||||
|
},
|
||||||
|
// footer: {
|
||||||
|
// type: 'void',
|
||||||
|
// 'x-component': 'Action.Drawer.Footer',
|
||||||
|
// properties: {
|
||||||
|
// cancel: {
|
||||||
|
// title: '{{t("Cancel")}}',
|
||||||
|
// 'x-component': 'Action',
|
||||||
|
// 'x-component-props': {
|
||||||
|
// useAction: '{{ cm.useCancelAction }}',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// submit: {
|
||||||
|
// title: '{{t("Submit")}}',
|
||||||
|
// 'x-component': 'Action',
|
||||||
|
// 'x-component-props': {
|
||||||
|
// type: 'primary',
|
||||||
|
// // useAction: '{{ useSetDepartments }}',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(department
|
||||||
|
? {
|
||||||
|
remove: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'RowRemoveAction',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addMembersSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-decorator': 'Form',
|
||||||
|
title: '{{t("Add members")}}',
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'AddMembersListProvider',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Filter") }}',
|
||||||
|
default: {
|
||||||
|
$and: [{ username: { $includes: '' } }, { nickname: { $includes: '' } }],
|
||||||
|
},
|
||||||
|
'x-action': 'filter',
|
||||||
|
'x-component': 'Filter.Action',
|
||||||
|
'x-use-component-props': 'useAddMembersFilterActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'FilterOutlined',
|
||||||
|
},
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Table.Void',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
onChange: '{{ handleSelect }}',
|
||||||
|
},
|
||||||
|
useDataSource: '{{ cm.useDataSourceFromRAC }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nickname: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
nickname: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
cancel: {
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
'x-use-component-props': 'useAddMembersActionProps',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userDepartmentsSchema = {
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
title: '{{t("Select Departments")}}',
|
||||||
|
'x-decorator': 'Form',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
properties: {
|
||||||
|
table: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DepartmentTable',
|
||||||
|
'x-component-props': {
|
||||||
|
useDataSource: '{{ useDataSource }}',
|
||||||
|
useDisabled: '{{ useDisabled }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
cancel: {
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useAddDepartments }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useAPIClient } from '@nocobase/client';
|
||||||
|
import { TreeManagerOptions, useTreeManager } from './tree-manager';
|
||||||
|
import deepmerge from 'deepmerge';
|
||||||
|
|
||||||
|
type DepartmentManagerOptions = {
|
||||||
|
resource?: string;
|
||||||
|
resourceOf?: string;
|
||||||
|
params?: any;
|
||||||
|
} & TreeManagerOptions;
|
||||||
|
|
||||||
|
export const useDepartmentManager = (options?: DepartmentManagerOptions) => {
|
||||||
|
const { resource = 'departments', resourceOf, params = {} } = options || {};
|
||||||
|
const api = useAPIClient();
|
||||||
|
const resourceAPI = api.resource(resource, resourceOf);
|
||||||
|
const treeManager = useTreeManager(options);
|
||||||
|
const { setTreeData, updateTreeData, setLoadedKeys, initData } = treeManager;
|
||||||
|
const loadData = async ({ key, children }) => {
|
||||||
|
if (children?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = await resourceAPI.list(
|
||||||
|
deepmerge(params, {
|
||||||
|
paginate: false,
|
||||||
|
appends: ['parent(recursively=true)'],
|
||||||
|
filter: {
|
||||||
|
parentId: key,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (!data?.data?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTreeData(updateTreeData(key, data?.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getByKeyword = async (keyword: string) => {
|
||||||
|
const { data } = await resourceAPI.list(
|
||||||
|
deepmerge(params, {
|
||||||
|
paginate: false,
|
||||||
|
filter: {
|
||||||
|
title: {
|
||||||
|
$includes: keyword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
appends: ['parent(recursively=true)'],
|
||||||
|
pageSize: 100,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
initData(data?.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...treeManager,
|
||||||
|
loadData,
|
||||||
|
getByKeyword,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
CollectionContext,
|
||||||
|
useActionContext,
|
||||||
|
useFilterFieldOptions,
|
||||||
|
useFilterFieldProps,
|
||||||
|
useResourceActionContext,
|
||||||
|
useResourceContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { useForm, useField } from '@formily/react';
|
||||||
|
|
||||||
|
export const useCreateAction = () => {
|
||||||
|
const form = useForm();
|
||||||
|
const field = useField();
|
||||||
|
const ctx = useActionContext();
|
||||||
|
const { refresh } = useResourceActionContext();
|
||||||
|
const { resource } = useResourceContext();
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
try {
|
||||||
|
await form.submit();
|
||||||
|
field.data = field.data || {};
|
||||||
|
field.data.loading = true;
|
||||||
|
await resource.create({ values: form.values });
|
||||||
|
ctx.setVisible(false);
|
||||||
|
await form.reset();
|
||||||
|
field.data.loading = false;
|
||||||
|
refresh();
|
||||||
|
} catch (error) {
|
||||||
|
if (field.data) {
|
||||||
|
field.data.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFilterActionProps = () => {
|
||||||
|
const collection = useContext(CollectionContext);
|
||||||
|
const options = useFilterFieldOptions(collection.fields);
|
||||||
|
const service = useResourceActionContext();
|
||||||
|
return useFilterFieldProps({
|
||||||
|
options,
|
||||||
|
params: service.state?.params?.[0] || service.params,
|
||||||
|
service,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from './tree-manager';
|
||||||
|
export * from './departments-manager';
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
export type TreeManagerOptions = {
|
||||||
|
label?: React.FC<{ node: any }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTreeManager = (options?: TreeManagerOptions) => {
|
||||||
|
const { label } = options || {};
|
||||||
|
const [treeData, setTreeData] = useState([]);
|
||||||
|
const [nodeMap, setNodeMap] = useState({});
|
||||||
|
const [expandedKeys, setExpandedKeys] = useState([]);
|
||||||
|
const [loadedKeys, setLoadedKeys] = useState([]);
|
||||||
|
|
||||||
|
const buildNodeMap = useCallback((data: any[]) => {
|
||||||
|
const mp = {};
|
||||||
|
const setNodeMapFromChild = (node: any) => {
|
||||||
|
let child = node ? { ...node } : null;
|
||||||
|
while (child) {
|
||||||
|
const parentId = child.parentId || 'root';
|
||||||
|
if (mp[parentId]) {
|
||||||
|
mp[parentId].childrenMap[child.id] = child;
|
||||||
|
} else {
|
||||||
|
mp[parentId] = {
|
||||||
|
...(child.parent || { id: parentId }),
|
||||||
|
childrenMap: {
|
||||||
|
[child.id]: child,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
child = child.parent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const setNodeMapFromParent = (node: any) => {
|
||||||
|
const childrenMap = {};
|
||||||
|
if (node.children && node.children.length) {
|
||||||
|
node.children.forEach((child: any) => {
|
||||||
|
childrenMap[child.id] = child;
|
||||||
|
setNodeMapFromParent(child);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mp[node.id] = {
|
||||||
|
...node,
|
||||||
|
childrenMap,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (!(data && data.length)) {
|
||||||
|
return mp;
|
||||||
|
}
|
||||||
|
data.forEach((node) => {
|
||||||
|
setNodeMapFromChild(node);
|
||||||
|
setNodeMapFromParent(node);
|
||||||
|
});
|
||||||
|
return mp;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const constructTreeData = useCallback((nodeMap: { [parentId: string | number]: any }) => {
|
||||||
|
const getChildren = (id: any) => {
|
||||||
|
if (!nodeMap[id]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (nodeMap[id].isLeaf) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Object.values(nodeMap[id]?.childrenMap || {}).map((node: any) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
title: label ? React.createElement(label, { node }) : node.title,
|
||||||
|
children: getChildren(node.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return getChildren('root');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initData = useCallback(
|
||||||
|
(data: any[]) => {
|
||||||
|
const mp = buildNodeMap(data);
|
||||||
|
setNodeMap(mp);
|
||||||
|
const treeData = constructTreeData(mp) || [];
|
||||||
|
setTreeData(treeData);
|
||||||
|
// setLoadedKeys([]);
|
||||||
|
},
|
||||||
|
[setTreeData, buildNodeMap, constructTreeData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateTreeData = (key: any, children: any[]) => {
|
||||||
|
const mp = buildNodeMap(children);
|
||||||
|
const newMap = { ...mp, ...nodeMap };
|
||||||
|
children.forEach((node) => {
|
||||||
|
newMap[key].childrenMap[node.id] = node;
|
||||||
|
});
|
||||||
|
setNodeMap(newMap);
|
||||||
|
return constructTreeData(newMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChildrenIds = useCallback(
|
||||||
|
(id: any) => {
|
||||||
|
if (!nodeMap[id]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const ids = [];
|
||||||
|
ids.push(...Object.keys(nodeMap[id].childrenMap).map((id) => Number(id)));
|
||||||
|
Object.keys(nodeMap[id].childrenMap).forEach((id) => {
|
||||||
|
ids.push(...getChildrenIds(id));
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
},
|
||||||
|
[nodeMap],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
initData,
|
||||||
|
treeData,
|
||||||
|
setTreeData,
|
||||||
|
nodeMap,
|
||||||
|
updateTreeData,
|
||||||
|
constructTreeData,
|
||||||
|
getChildrenIds,
|
||||||
|
loadedKeys,
|
||||||
|
setLoadedKeys,
|
||||||
|
expandedKeys,
|
||||||
|
setExpandedKeys,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ArrayField } from '@formily/core';
|
||||||
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
useTableBlockContext,
|
||||||
|
findFilterTargets,
|
||||||
|
DataBlock,
|
||||||
|
useFilterBlock,
|
||||||
|
mergeFilter,
|
||||||
|
removeNullCondition,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
|
||||||
|
export const useTableBlockProps = () => {
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const ctx = useTableBlockContext();
|
||||||
|
const { getDataBlocks } = useFilterBlock();
|
||||||
|
const isLoading = ctx?.service?.loading;
|
||||||
|
|
||||||
|
const ctxRef = useRef(null);
|
||||||
|
ctxRef.current = ctx;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading) {
|
||||||
|
const serviceResponse = ctx?.service?.data;
|
||||||
|
const data = serviceResponse?.data || [];
|
||||||
|
const meta = serviceResponse?.meta || {};
|
||||||
|
const selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
|
||||||
|
|
||||||
|
if (!isEqual(field.value, data)) {
|
||||||
|
field.value = data;
|
||||||
|
field?.setInitialValue(data);
|
||||||
|
}
|
||||||
|
field.data = field.data || {};
|
||||||
|
|
||||||
|
if (!isEqual(field.data.selectedRowKeys, selectedRowKeys)) {
|
||||||
|
field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
field.componentProps.pagination = field.componentProps.pagination || {};
|
||||||
|
field.componentProps.pagination.pageSize = meta?.pageSize;
|
||||||
|
field.componentProps.pagination.total = meta?.count;
|
||||||
|
field.componentProps.pagination.current = meta?.page;
|
||||||
|
}
|
||||||
|
}, [field, ctx?.service?.data, isLoading, ctx?.field?.data?.selectedRowKeys]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bordered: ctx.bordered,
|
||||||
|
childrenColumnName: ctx.childrenColumnName,
|
||||||
|
loading: ctx?.service?.loading,
|
||||||
|
showIndex: ctx.showIndex,
|
||||||
|
dragSort: ctx.dragSort && ctx.dragSortBy,
|
||||||
|
rowKey: ctx.rowKey || fieldSchema?.['x-component-props']?.rowKey || 'id',
|
||||||
|
pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination,
|
||||||
|
onRowSelectionChange: useCallback((selectedRowKeys, selectedRowData) => {
|
||||||
|
ctx.field.data = ctx?.field?.data || {};
|
||||||
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
ctx.field.data.selectedRowData = selectedRowData;
|
||||||
|
ctx?.field?.onRowSelect?.(selectedRowKeys);
|
||||||
|
}, []),
|
||||||
|
onRowDragEnd: useCallback(
|
||||||
|
async ({ from, to }) => {
|
||||||
|
await ctx.resource.move({
|
||||||
|
sourceId: from[ctx.rowKey || 'id'],
|
||||||
|
targetId: to[ctx.rowKey || 'id'],
|
||||||
|
sortField: ctx.dragSort && ctx.dragSortBy,
|
||||||
|
});
|
||||||
|
ctx.service.refresh();
|
||||||
|
// ctx.resource
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
},
|
||||||
|
[ctx.rowKey, ctx.dragSort, ctx.dragSortBy],
|
||||||
|
),
|
||||||
|
onChange: useCallback(
|
||||||
|
({ current, pageSize }, filters, sorter) => {
|
||||||
|
const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort'];
|
||||||
|
const sort = sorter.order
|
||||||
|
? sorter.order === `ascend`
|
||||||
|
? [sorter.field]
|
||||||
|
: [`-${sorter.field}`]
|
||||||
|
: globalSort || ctxRef.current.dragSortBy;
|
||||||
|
const currentPageSize = pageSize || fieldSchema.parent?.['x-decorator-props']?.['params']?.pageSize;
|
||||||
|
const args = { ...ctxRef.current?.service?.params?.[0], page: current || 1, pageSize: currentPageSize };
|
||||||
|
if (sort) {
|
||||||
|
args['sort'] = sort;
|
||||||
|
}
|
||||||
|
ctxRef.current?.service.run(args);
|
||||||
|
},
|
||||||
|
[fieldSchema.parent],
|
||||||
|
),
|
||||||
|
onClickRow: useCallback(
|
||||||
|
(record, setSelectedRow, selectedRow) => {
|
||||||
|
const { targets, uid } = findFilterTargets(fieldSchema);
|
||||||
|
const dataBlocks = getDataBlocks();
|
||||||
|
|
||||||
|
// 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错
|
||||||
|
if (!targets || !targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid))) {
|
||||||
|
// 当用户已经点击过某一行,如果此时再把相连接的区块给删除的话,行的高亮状态就会一直保留。
|
||||||
|
// 这里暂时没有什么比较好的方法,只是在用户再次点击的时候,把高亮状态给清除掉。
|
||||||
|
setSelectedRow((prev) => (prev.length ? [] : prev));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBlock = dataBlocks.find((block) => block.uid === fieldSchema.parent['x-uid']);
|
||||||
|
|
||||||
|
dataBlocks.forEach((block) => {
|
||||||
|
const target = targets.find((target) => target.uid === block.uid);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const isForeignKey = block.foreignKeyFields?.some((field) => field.name === target.field);
|
||||||
|
const sourceKey = getSourceKey(currentBlock, target.field);
|
||||||
|
const recordKey = isForeignKey ? sourceKey : ctx.rowKey;
|
||||||
|
const value = [record[recordKey]];
|
||||||
|
|
||||||
|
const param = block.service.params?.[0] || {};
|
||||||
|
// 保留原有的 filter
|
||||||
|
const storedFilter = block.service.params?.[1]?.filters || {};
|
||||||
|
|
||||||
|
if (selectedRow.includes(record[ctx.rowKey])) {
|
||||||
|
if (block.dataLoadingMode === 'manual') {
|
||||||
|
return block.clearData();
|
||||||
|
}
|
||||||
|
delete storedFilter[uid];
|
||||||
|
} else {
|
||||||
|
storedFilter[uid] = {
|
||||||
|
$and: [
|
||||||
|
{
|
||||||
|
[target.field || ctx.rowKey]: {
|
||||||
|
[target.field ? '$in' : '$eq']: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedFilter = mergeFilter([
|
||||||
|
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
|
||||||
|
block.defaultFilter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return block.doFilter(
|
||||||
|
{
|
||||||
|
...param,
|
||||||
|
page: 1,
|
||||||
|
filter: mergedFilter,
|
||||||
|
},
|
||||||
|
{ filters: storedFilter },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新表格的选中状态
|
||||||
|
setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [record[ctx.rowKey]]));
|
||||||
|
},
|
||||||
|
[ctx.rowKey, fieldSchema, getDataBlocks],
|
||||||
|
),
|
||||||
|
onExpand: useCallback((expanded, record) => {
|
||||||
|
ctx?.field.onExpandClick?.(expanded, record);
|
||||||
|
}, []),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSourceKey(currentBlock: DataBlock, field: string) {
|
||||||
|
const associationField = currentBlock?.associatedFields?.find((item) => item.foreignKey === field);
|
||||||
|
return associationField?.sourceKey || 'id';
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Plugin, SchemaComponentContext, useSchemaComponentContext } from '@nocobase/client';
|
||||||
|
import { tval } from '@nocobase/utils/client';
|
||||||
|
import { DepartmentBlock } from './departments/DepartmentBlock';
|
||||||
|
import React from 'react';
|
||||||
|
import { ResourcesProvider } from './ResourcesProvider';
|
||||||
|
import ACLPlugin from '@nocobase/plugin-acl/client';
|
||||||
|
import { RoleDepartmentsManager } from './roles/RoleDepartmentsManager';
|
||||||
|
import {
|
||||||
|
UserDepartmentsFieldSettings,
|
||||||
|
ReadOnlyAssociationField,
|
||||||
|
UserMainDepartmentFieldSettings,
|
||||||
|
DepartmentOwnersFieldSettings,
|
||||||
|
} from './components';
|
||||||
|
|
||||||
|
export class PluginDepartmentsClient extends Plugin {
|
||||||
|
async afterAdd() {
|
||||||
|
// await this.app.pm.add()
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeLoad() {}
|
||||||
|
|
||||||
|
// You can get and modify the app instance here
|
||||||
|
async load() {
|
||||||
|
this.app.addComponents({
|
||||||
|
UserDepartmentsField: ReadOnlyAssociationField,
|
||||||
|
UserMainDepartmentField: ReadOnlyAssociationField,
|
||||||
|
DepartmentOwnersField: ReadOnlyAssociationField,
|
||||||
|
});
|
||||||
|
this.app.schemaSettingsManager.add(UserDepartmentsFieldSettings);
|
||||||
|
this.app.schemaSettingsManager.add(UserMainDepartmentFieldSettings);
|
||||||
|
this.app.schemaSettingsManager.add(DepartmentOwnersFieldSettings);
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('users-permissions.departments', {
|
||||||
|
icon: 'ApartmentOutlined',
|
||||||
|
title: tval('Departments', { ns: 'departments' }),
|
||||||
|
Component: () => {
|
||||||
|
const scCtx = useSchemaComponentContext();
|
||||||
|
return (
|
||||||
|
<SchemaComponentContext.Provider value={{ ...scCtx, designable: false }}>
|
||||||
|
<ResourcesProvider>
|
||||||
|
<DepartmentBlock />
|
||||||
|
</ResourcesProvider>
|
||||||
|
</SchemaComponentContext.Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sort: 2,
|
||||||
|
aclSnippet: 'pm.departments',
|
||||||
|
});
|
||||||
|
|
||||||
|
const acl = this.app.pm.get(ACLPlugin);
|
||||||
|
acl.rolesManager.add('departments', {
|
||||||
|
title: tval('Departments', { ns: 'departments' }),
|
||||||
|
Component: RoleDepartmentsManager,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginDepartmentsClient;
|
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function useDepartmentTranslation() {
|
||||||
|
return useTranslation(['departments', 'client'], { nsMode: 'fallback' });
|
||||||
|
}
|
@ -0,0 +1,178 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useContext, useEffect, useMemo } from 'react';
|
||||||
|
import { App } from 'antd';
|
||||||
|
import { useDepartmentTranslation } from '../locale';
|
||||||
|
import {
|
||||||
|
CollectionManagerContext,
|
||||||
|
CollectionProvider_deprecated,
|
||||||
|
ResourceActionContext,
|
||||||
|
SchemaComponent,
|
||||||
|
useAPIClient,
|
||||||
|
useActionContext,
|
||||||
|
useRecord,
|
||||||
|
useRequest,
|
||||||
|
useResourceActionContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { RolesManagerContext } from '@nocobase/plugin-acl/client';
|
||||||
|
import { departmentCollection } from '../collections/departments';
|
||||||
|
import { getDepartmentsSchema } from './schemas/departments';
|
||||||
|
import { useFilterActionProps } from '../hooks';
|
||||||
|
import { DepartmentTable } from '../departments/DepartmentTable';
|
||||||
|
import { useForm } from '@formily/react';
|
||||||
|
|
||||||
|
const useRemoveDepartment = () => {
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { role } = useContext(RolesManagerContext);
|
||||||
|
const { id } = useRecord();
|
||||||
|
const { refresh } = useResourceActionContext();
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
await api.resource(`roles/${role?.name}/departments`).remove({
|
||||||
|
values: [id],
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useBulkRemoveDepartments = () => {
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { state, setState, refresh } = useResourceActionContext();
|
||||||
|
const { role } = useContext(RolesManagerContext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
const selected = state?.selectedRowKeys;
|
||||||
|
if (!selected?.length) {
|
||||||
|
message.warning(t('Please select departments'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.resource(`roles/${role?.name}/departments`).remove({
|
||||||
|
values: selected,
|
||||||
|
});
|
||||||
|
setState?.({ selectedRowKeys: [] });
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const DepartmentTitle: React.FC = () => {
|
||||||
|
const record = useRecord();
|
||||||
|
const getTitle = (record: any) => {
|
||||||
|
const title = record.title;
|
||||||
|
const parent = record.parent;
|
||||||
|
if (parent) {
|
||||||
|
return getTitle(parent) + ' / ' + title;
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>{getTitle(record)}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDataSource = (options?: any) => {
|
||||||
|
const defaultRequest = {
|
||||||
|
resource: 'departments',
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
// filter: {
|
||||||
|
// parentId: null,
|
||||||
|
// },
|
||||||
|
appends: ['roles', 'parent(recursively=true)'],
|
||||||
|
sort: ['createdAt'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const service = useRequest(defaultRequest, options);
|
||||||
|
return {
|
||||||
|
...service,
|
||||||
|
defaultRequest,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDisabled = () => {
|
||||||
|
const { role } = useContext(RolesManagerContext);
|
||||||
|
return {
|
||||||
|
disabled: (record: any) => record?.roles?.some((r: { name: string }) => r.name === role?.name),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAddDepartments = () => {
|
||||||
|
const { role } = useContext(RolesManagerContext);
|
||||||
|
const api = useAPIClient();
|
||||||
|
const form = useForm();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
const { refresh } = useResourceActionContext();
|
||||||
|
const { departments } = form.values || {};
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
await api.resource('roles.departments', role.name).add({
|
||||||
|
values: departments.map((dept: any) => dept.id),
|
||||||
|
});
|
||||||
|
form.reset();
|
||||||
|
setVisible(false);
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoleDepartmentsManager: React.FC = () => {
|
||||||
|
const { t } = useDepartmentTranslation();
|
||||||
|
const { role } = useContext(RolesManagerContext);
|
||||||
|
const service = useRequest(
|
||||||
|
{
|
||||||
|
resource: `roles/${role?.name}/departments`,
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
appends: ['parent', 'parent.parent(recursively=true)'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ready: !!role,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
service.run();
|
||||||
|
}, [role]);
|
||||||
|
|
||||||
|
const schema = useMemo(() => getDepartmentsSchema(), [role]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResourceActionContext.Provider value={{ ...service }}>
|
||||||
|
<CollectionProvider_deprecated collection={departmentCollection}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={schema}
|
||||||
|
components={{ DepartmentTable, DepartmentTitle }}
|
||||||
|
scope={{
|
||||||
|
useFilterActionProps,
|
||||||
|
t,
|
||||||
|
useRemoveDepartment,
|
||||||
|
useBulkRemoveDepartments,
|
||||||
|
useDataSource,
|
||||||
|
useDisabled,
|
||||||
|
useAddDepartments,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CollectionProvider_deprecated>
|
||||||
|
</ResourceActionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
|
||||||
|
export const getDepartmentsSchema = () => ({
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Filter") }}',
|
||||||
|
'x-action': 'filter',
|
||||||
|
'x-component': 'Filter.Action',
|
||||||
|
'x-use-component-props': 'useFilterActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'FilterOutlined',
|
||||||
|
},
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
properties: {
|
||||||
|
remove: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Remove")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'MinusOutlined',
|
||||||
|
confirm: {
|
||||||
|
title: "{{t('Remove')}}",
|
||||||
|
content: "{{t('Are you sure you want to remove these departments?')}}",
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
useAction: '{{ useBulkRemoveDepartments }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Add departments")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'PlusOutlined',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-decorator': 'FormV2',
|
||||||
|
title: '{{t("Add departments")}}',
|
||||||
|
properties: {
|
||||||
|
table: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DepartmentTable',
|
||||||
|
'x-component-props': {
|
||||||
|
useDataSource: '{{ useDataSource }}',
|
||||||
|
useDisabled: '{{ useDisabled }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
cancel: {
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useAddDepartments }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Table.Void',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
useDataSource: '{{ cm.useDataSourceFromRAC }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Department name")}}',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'DepartmentTitle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Actions")}}',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
'x-component-props': {
|
||||||
|
split: '|',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
remove: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Remove") }}',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-component-props': {
|
||||||
|
confirm: {
|
||||||
|
title: "{{t('Remove department')}}",
|
||||||
|
content: "{{t('Are you sure you want to remove it?')}}",
|
||||||
|
},
|
||||||
|
useAction: '{{ useRemoveDepartment }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getDepartmentTitle = (record: any) => {
|
||||||
|
const title = record.title;
|
||||||
|
const parent = record.parent;
|
||||||
|
if (parent) {
|
||||||
|
return getDepartmentTitle(parent) + ' / ' + title;
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
};
|
20
packages/plugins/@nocobase/plugin-departments/src/index.ts
Normal file
20
packages/plugins/@nocobase/plugin-departments/src/index.ts
Normal file
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './server';
|
||||||
|
export { default } from './server';
|
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"Department": "Department",
|
||||||
|
"All users": "All users",
|
||||||
|
"New department": "New department",
|
||||||
|
"Add department": "Add department",
|
||||||
|
"Add departments": "Add departments",
|
||||||
|
"New sub department": "New sub department",
|
||||||
|
"Edit department": "Edit department",
|
||||||
|
"Delete department": "Delete department",
|
||||||
|
"Departments": "Departments",
|
||||||
|
"Main department": "Main department",
|
||||||
|
"Owner": "Owner",
|
||||||
|
"Department name": "Department name",
|
||||||
|
"Superior department": "Superior department",
|
||||||
|
"Owners": "Owners",
|
||||||
|
"Add members": "Add members",
|
||||||
|
"Search for departments, users": "Search for departments, users",
|
||||||
|
"Search results": "Search results",
|
||||||
|
"Departments management": "Departments management",
|
||||||
|
"Roles management": "Roles management",
|
||||||
|
"Remove members": "Remove members",
|
||||||
|
"Remove member": "Remove member",
|
||||||
|
"Remove departments": "Remove departments",
|
||||||
|
"Remove department": "Remove department",
|
||||||
|
"Are you sure you want to remove it?": "Are you sure you want to remove it?",
|
||||||
|
"Are you sure you want to remove these members?": "Are you sure you want to remove these members?",
|
||||||
|
"Are you sure you want to remove these departments?": "Are you sure you want to remove these departments?",
|
||||||
|
"Please select members": "Please select members",
|
||||||
|
"Please select departments": "Please select departments",
|
||||||
|
"The department has sub-departments, please delete them first": "The department has sub-departments, please delete them first",
|
||||||
|
"The department has members, please remove them first": "The department has members, please remove them first",
|
||||||
|
"Main": "Main",
|
||||||
|
"Set as main department": "Set as main department",
|
||||||
|
"This field is currently not supported for use in form blocks.": "This field is currently not supported for use in form blocks."
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"Department": "部门",
|
||||||
|
"All users": "所有用户",
|
||||||
|
"New department": "新建部门",
|
||||||
|
"New sub department": "新建子部门",
|
||||||
|
"Add department": "添加部门",
|
||||||
|
"Add departments": "添加部门",
|
||||||
|
"Edit department": "编辑部门",
|
||||||
|
"Delete department": "删除部门",
|
||||||
|
"Departments": "部门",
|
||||||
|
"Main department": "主属部门",
|
||||||
|
"Owner": "负责人",
|
||||||
|
"Department name": "部门名称",
|
||||||
|
"Superior department": "上级部门",
|
||||||
|
"Owners": "负责人",
|
||||||
|
"Add members": "添加成员",
|
||||||
|
"Search for departments, users": "搜索部门、用户",
|
||||||
|
"Search results": "搜索结果",
|
||||||
|
"Departments management": "部门管理",
|
||||||
|
"Roles management": "角色管理",
|
||||||
|
"Remove members": "移除成员",
|
||||||
|
"Remove member": "移除成员",
|
||||||
|
"Remove departments": "移除部门",
|
||||||
|
"Remove department": "移除部门",
|
||||||
|
"Are you sure you want to remove it?": "你确定要移除吗?",
|
||||||
|
"Are you sure you want to remove these members?": "你确定要移除这些成员吗?",
|
||||||
|
"Are you sure you want to remove these departments?": "你确定要移除这些部门吗?",
|
||||||
|
"Please select members": "请选择成员",
|
||||||
|
"Please select departments": "请选择部门",
|
||||||
|
"The department has sub-departments, please delete them first": "部门下有子部门,请先删除子部门",
|
||||||
|
"The department has members, please remove them first": "部门下有成员,请先移除",
|
||||||
|
"Main": "主属部门",
|
||||||
|
"Set as main department": "设置为主属部门",
|
||||||
|
"This field is currently not supported for use in form blocks.": "该字段目前不支持在表单区块中使用。"
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Database, Repository } from '@nocobase/database';
|
||||||
|
import { MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
|
||||||
|
describe('actions', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
let repo: Repository;
|
||||||
|
let agent: any;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['field-sort', 'users', 'departments'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
repo = db.getRepository('departments');
|
||||||
|
agent = app.agent();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await repo.destroy({ truncate: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list users exclude department', async () => {
|
||||||
|
const dept = await repo.create({
|
||||||
|
values: {
|
||||||
|
title: 'Test department',
|
||||||
|
members: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = await agent.resource('users').listExcludeDept({
|
||||||
|
departmentId: dept.id,
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list users exclude department with filter', async () => {
|
||||||
|
let res = await agent.resource('users').listExcludeDept({
|
||||||
|
departmentId: 1,
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.data.length).toBe(1);
|
||||||
|
|
||||||
|
res = await agent.resource('users').listExcludeDept({
|
||||||
|
departmentId: 1,
|
||||||
|
filter: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.data.length).toBe(1);
|
||||||
|
|
||||||
|
res = await agent.resource('users').listExcludeDept({
|
||||||
|
departmentId: 1,
|
||||||
|
filter: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set main department', async () => {
|
||||||
|
const depts = await repo.create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
title: 'Dept1',
|
||||||
|
members: [1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Dept2',
|
||||||
|
members: [1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const deptUsers = db.getRepository('departmentsUsers');
|
||||||
|
await deptUsers.update({
|
||||||
|
filter: {
|
||||||
|
departmentId: depts[0].id,
|
||||||
|
userId: 1,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = await agent.resource('users').setMainDepartment({
|
||||||
|
values: {
|
||||||
|
userId: 1,
|
||||||
|
departmentId: depts[1].id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const records = await deptUsers.find({
|
||||||
|
filter: {
|
||||||
|
userId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dept1 = records.find((record: any) => record.departmentId === depts[0].id);
|
||||||
|
const dept2 = records.find((record: any) => record.departmentId === depts[1].id);
|
||||||
|
expect(dept1.isMain).toBe(false);
|
||||||
|
expect(dept2.isMain).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,278 @@
|
|||||||
|
/**
|
||||||
|
* 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 PluginUserDataSyncServer, { UserDataResourceManager } from '@nocobase/plugin-user-data-sync';
|
||||||
|
import { MockDatabase, MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
|
||||||
|
describe('department data sync', async () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: MockDatabase;
|
||||||
|
let resourceManager: UserDataResourceManager;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['field-sort', 'user-data-sync', 'users', 'departments'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
const plugin = app.pm.get('user-data-sync') as PluginUserDataSyncServer;
|
||||||
|
resourceManager = plugin.resourceManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.clean({ drop: true });
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create department', async () => {
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'department',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: '2',
|
||||||
|
title: 'sub-test',
|
||||||
|
parentUid: '1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const department = await db.getRepository('departments').findOne({
|
||||||
|
filter: {
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
appends: ['children'],
|
||||||
|
});
|
||||||
|
expect(department).toBeTruthy();
|
||||||
|
expect(department.title).toBe('test');
|
||||||
|
expect(department.children).toHaveLength(1);
|
||||||
|
expect(department.children[0].title).toBe('sub-test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update department', async () => {
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'department',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const department = await db.getRepository('departments').findOne({
|
||||||
|
filter: {
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
appends: ['children'],
|
||||||
|
});
|
||||||
|
expect(department).toBeTruthy();
|
||||||
|
expect(department.children).toHaveLength(0);
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'department',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
title: 'test2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: '2',
|
||||||
|
title: 'sub-test',
|
||||||
|
parentUid: '1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const department2 = await db.getRepository('departments').findOne({
|
||||||
|
filter: {
|
||||||
|
id: department.id,
|
||||||
|
},
|
||||||
|
appends: ['children'],
|
||||||
|
});
|
||||||
|
expect(department2).toBeTruthy();
|
||||||
|
expect(department2.title).toBe('test2');
|
||||||
|
expect(department2.children).toHaveLength(1);
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'department',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '2',
|
||||||
|
title: 'sub-test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const department3 = await db.getRepository('departments').findOne({
|
||||||
|
filter: {
|
||||||
|
id: department2.children[0].id,
|
||||||
|
},
|
||||||
|
appends: ['parent'],
|
||||||
|
});
|
||||||
|
expect(department3).toBeTruthy();
|
||||||
|
expect(department3.parent).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update user department', async () => {
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'department',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '12',
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const department = await db.getRepository('departments').findOne({
|
||||||
|
filter: {
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(department).toBeTruthy();
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
departments: ['12'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const user = await db.getRepository('users').findOne({
|
||||||
|
filter: {
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
},
|
||||||
|
appends: ['departments'],
|
||||||
|
});
|
||||||
|
expect(user).toBeTruthy();
|
||||||
|
expect(user.departments).toHaveLength(1);
|
||||||
|
expect(user.departments[0].id).toBe(department.id);
|
||||||
|
const departmentUser = await db.getRepository('departmentsUsers').findOne({
|
||||||
|
filter: {
|
||||||
|
departmentId: department.id,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(departmentUser).toBeTruthy();
|
||||||
|
expect(departmentUser.isOwner).toBe(false);
|
||||||
|
expect(departmentUser.isMain).toBe(false);
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
departments: [
|
||||||
|
{
|
||||||
|
uid: '12',
|
||||||
|
isOwner: true,
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const departmentUser2 = await db.getRepository('departmentsUsers').findOne({
|
||||||
|
filter: {
|
||||||
|
departmentId: department.id,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(departmentUser2).toBeTruthy();
|
||||||
|
expect(departmentUser2.isOwner).toBe(true);
|
||||||
|
expect(departmentUser2.isMain).toBe(true);
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '2',
|
||||||
|
nickname: 'test2',
|
||||||
|
email: 'test2@nocobase.com',
|
||||||
|
departments: [
|
||||||
|
{
|
||||||
|
uid: '12',
|
||||||
|
isOwner: true,
|
||||||
|
isMain: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const user2 = await db.getRepository('users').findOne({
|
||||||
|
filter: {
|
||||||
|
email: 'test2@nocobase.com',
|
||||||
|
},
|
||||||
|
appends: ['departments'],
|
||||||
|
});
|
||||||
|
expect(user2).toBeTruthy();
|
||||||
|
expect(user2.departments).toHaveLength(1);
|
||||||
|
expect(user2.departments[0].id).toBe(department.id);
|
||||||
|
const departmentUser3 = await db.getRepository('departmentsUsers').findOne({
|
||||||
|
filter: {
|
||||||
|
departmentId: department.id,
|
||||||
|
userId: user2.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(departmentUser3).toBeTruthy();
|
||||||
|
expect(departmentUser3.isOwner).toBe(true);
|
||||||
|
expect(departmentUser3.isMain).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update department custom field', async () => {
|
||||||
|
const departmemntCollection = db.getCollection('departments');
|
||||||
|
departmemntCollection.addField('customField', { type: 'string' });
|
||||||
|
await db.sync({
|
||||||
|
alter: true,
|
||||||
|
});
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'department',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
title: 'test',
|
||||||
|
customField: 'testField',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const department = await db.getRepository('departments').findOne({
|
||||||
|
filter: {
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(department).toBeTruthy();
|
||||||
|
expect(department.customField).toBe('testField');
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'department',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
title: 'test',
|
||||||
|
customField: 'testField2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const department2 = await db.getRepository('departments').findOne({
|
||||||
|
filter: {
|
||||||
|
id: department.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(department2).toBeTruthy();
|
||||||
|
expect(department2.customField).toBe('testField2');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Database, Repository } from '@nocobase/database';
|
||||||
|
import { MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
|
||||||
|
describe('destroy department check', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
let repo: Repository;
|
||||||
|
let agent: any;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['field-sort', 'users', 'departments'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
repo = db.getRepository('departments');
|
||||||
|
agent = app.agent();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await repo.destroy({ truncate: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if it has sub departments', async () => {
|
||||||
|
const dept = await repo.create({
|
||||||
|
values: {
|
||||||
|
title: 'Department',
|
||||||
|
children: [{ title: 'Sub department' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = await agent.resource('departments').destroy({
|
||||||
|
filterByTk: dept.id,
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.text).toBe('The department has sub-departments, please delete them first');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if it has members', async () => {
|
||||||
|
const dept = await repo.create({
|
||||||
|
values: {
|
||||||
|
title: 'Department',
|
||||||
|
members: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = await agent.resource('departments').destroy({
|
||||||
|
filterByTk: dept.id,
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.text).toBe('The department has members, please remove them first');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Database, Repository } from '@nocobase/database';
|
||||||
|
import { MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
|
||||||
|
describe('set department owners', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
let repo: Repository;
|
||||||
|
let agent: any;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['field-sort', 'users', 'departments'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
repo = db.getRepository('departments');
|
||||||
|
agent = app.agent();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await repo.destroy({ truncate: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set department owners', async () => {
|
||||||
|
await db.getRepository('users').create({
|
||||||
|
values: {
|
||||||
|
username: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dept = await repo.create({
|
||||||
|
values: {
|
||||||
|
title: 'Department',
|
||||||
|
members: [1, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await agent.resource('departments').update({
|
||||||
|
filterByTk: dept.id,
|
||||||
|
values: {
|
||||||
|
owners: [{ id: 1 }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const deptUser = await db.getRepository('departmentsUsers').findOne({
|
||||||
|
filter: {
|
||||||
|
userId: 1,
|
||||||
|
departmentId: dept.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(deptUser.isOwner).toBe(true);
|
||||||
|
await agent.resource('departments').update({
|
||||||
|
filterByTk: dept.id,
|
||||||
|
values: {
|
||||||
|
owners: [{ id: 2 }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const deptUser1 = await db.getRepository('departmentsUsers').findOne({
|
||||||
|
filter: {
|
||||||
|
userId: 1,
|
||||||
|
departmentId: dept.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(deptUser1.isOwner).toBe(false);
|
||||||
|
const deptUser2 = await db.getRepository('departmentsUsers').findOne({
|
||||||
|
filter: {
|
||||||
|
userId: 2,
|
||||||
|
departmentId: dept.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(deptUser2.isOwner).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Database, Repository } from '@nocobase/database';
|
||||||
|
import { MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
import { setDepartmentsInfo } from '../middlewares';
|
||||||
|
|
||||||
|
describe('set departments info', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
let repo: Repository;
|
||||||
|
let agent: any;
|
||||||
|
let ctx: any;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['field-sort', 'users', 'departments', 'acl', 'data-source-manager'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
repo = db.getRepository('departments');
|
||||||
|
agent = app.agent();
|
||||||
|
ctx = {
|
||||||
|
db,
|
||||||
|
cache: app.cache,
|
||||||
|
state: {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await repo.destroy({ truncate: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set departments roles', async () => {
|
||||||
|
ctx.state.currentUser = await db.getRepository('users').findOne({
|
||||||
|
filterByTk: 1,
|
||||||
|
});
|
||||||
|
const role = await db.getRepository('roles').create({
|
||||||
|
values: {
|
||||||
|
name: 'test-role',
|
||||||
|
title: 'Test role',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await repo.create({
|
||||||
|
values: {
|
||||||
|
title: 'Department',
|
||||||
|
roles: [role.name],
|
||||||
|
members: [1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await setDepartmentsInfo(ctx, () => {});
|
||||||
|
expect(ctx.state.attachRoles.length).toBe(1);
|
||||||
|
expect(ctx.state.attachRoles[0].name).toBe('test-role');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Database, Repository } from '@nocobase/database';
|
||||||
|
import { MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
|
||||||
|
describe('set main department', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
let repo: Repository;
|
||||||
|
let agent: any;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['field-sort', 'users', 'departments'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
repo = db.getRepository('departments');
|
||||||
|
agent = app.agent();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await repo.destroy({ truncate: true });
|
||||||
|
await db.getRepository('departmentsUsers').destroy({ truncate: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set main department when add department members', async () => {
|
||||||
|
const dept = await repo.create({
|
||||||
|
values: {
|
||||||
|
title: 'Department',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await db.getRepository('users').create({
|
||||||
|
values: {
|
||||||
|
username: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await agent.resource('departments.members', dept.id).add({
|
||||||
|
values: [1, 2],
|
||||||
|
});
|
||||||
|
const throughRepo = db.getRepository('departmentsUsers');
|
||||||
|
const deptUsers = await throughRepo.find({
|
||||||
|
filter: {
|
||||||
|
userId: {
|
||||||
|
$in: [1, 2],
|
||||||
|
},
|
||||||
|
departmentId: dept.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const item of deptUsers) {
|
||||||
|
expect(item.isMain).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dept2 = await repo.create({
|
||||||
|
values: {
|
||||||
|
title: 'Department2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await agent.resource('departments.members', dept2.id).add({
|
||||||
|
values: [2],
|
||||||
|
});
|
||||||
|
const deptUsers2 = await throughRepo.find({
|
||||||
|
filter: {
|
||||||
|
userId: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(deptUsers2.length).toBe(2);
|
||||||
|
expect(deptUsers2.find((i: any) => i.departmentId === dept.id).isMain).toBe(true);
|
||||||
|
expect(deptUsers2.find((i: any) => i.departmentId === dept2.id).isMain).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set main department when remove department members', async () => {
|
||||||
|
const depts = await repo.create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
title: 'Department',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Department2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await agent.resource('departments.members', depts[0].id).add({
|
||||||
|
values: [1],
|
||||||
|
});
|
||||||
|
await agent.resource('departments.members', depts[1].id).add({
|
||||||
|
values: [1],
|
||||||
|
});
|
||||||
|
const throughRepo = db.getRepository('departmentsUsers');
|
||||||
|
const deptUsers = await throughRepo.find({
|
||||||
|
filter: {
|
||||||
|
userId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(deptUsers.length).toBe(2);
|
||||||
|
expect(deptUsers.find((i: any) => i.departmentId === depts[0].id).isMain).toBe(true);
|
||||||
|
expect(deptUsers.find((i: any) => i.departmentId === depts[1].id).isMain).toBe(false);
|
||||||
|
|
||||||
|
await agent.resource('departments.members', depts[0].id).remove({
|
||||||
|
values: [1],
|
||||||
|
});
|
||||||
|
const deptUsers2 = await throughRepo.find({
|
||||||
|
filter: {
|
||||||
|
userId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(deptUsers2.length).toBe(1);
|
||||||
|
expect(deptUsers2[0].departmentId).toBe(depts[1].id);
|
||||||
|
expect(deptUsers2[0].isMain).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set main department when add user departments', async () => {
|
||||||
|
const depts = await repo.create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
title: 'Department',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Department2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await agent.resource('users.departments', 1).add({
|
||||||
|
values: depts.map((dept: any) => dept.id),
|
||||||
|
});
|
||||||
|
const throughRepo = db.getRepository('departmentsUsers');
|
||||||
|
const deptUsers = await throughRepo.find({
|
||||||
|
filter: {
|
||||||
|
userId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(deptUsers.length).toBe(2);
|
||||||
|
expect(deptUsers.find((i: any) => i.departmentId === depts[0].id).isMain).toBe(true);
|
||||||
|
expect(deptUsers.find((i: any) => i.departmentId === depts[1].id).isMain).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set main department when remove user departments', async () => {
|
||||||
|
const depts = await repo.create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
title: 'Department',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Department2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await agent.resource('users.departments', 1).add({
|
||||||
|
values: depts.map((dept: any) => dept.id),
|
||||||
|
});
|
||||||
|
await agent.resource('users.departments', 1).remove({
|
||||||
|
values: [depts[0].id],
|
||||||
|
});
|
||||||
|
const throughRepo = db.getRepository('departmentsUsers');
|
||||||
|
const deptUsers = await throughRepo.find({
|
||||||
|
filter: {
|
||||||
|
userId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(deptUsers.length).toBe(1);
|
||||||
|
expect(deptUsers[0].departmentId).toBe(depts[1].id);
|
||||||
|
expect(deptUsers[0].isMain).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Database, Repository } from '@nocobase/database';
|
||||||
|
import { MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
|
||||||
|
describe('update department isLeaf', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
let repo: Repository;
|
||||||
|
let agent: any;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['field-sort', 'users', 'departments'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
repo = db.getRepository('departments');
|
||||||
|
agent = app.agent();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await repo.destroy({ truncate: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update isLeaf when create sub department', async () => {
|
||||||
|
const res = await agent.resource('departments').create({
|
||||||
|
values: {
|
||||||
|
title: 'Department',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dept = res.body.data;
|
||||||
|
expect(dept).toBeTruthy();
|
||||||
|
expect(dept.isLeaf).toBe(true);
|
||||||
|
|
||||||
|
await agent.resource('departments').create({
|
||||||
|
values: {
|
||||||
|
title: 'Sub Department',
|
||||||
|
parent: dept,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const record = await repo.findOne({
|
||||||
|
filterByTk: dept.id,
|
||||||
|
});
|
||||||
|
expect(record.isLeaf).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update isLeaf when update department', async () => {
|
||||||
|
const res = await agent.resource('departments').create({
|
||||||
|
values: {
|
||||||
|
title: 'Department',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res2 = await agent.resource('departments').create({
|
||||||
|
values: {
|
||||||
|
title: 'Department2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dept1 = res.body.data;
|
||||||
|
const dept2 = res2.body.data;
|
||||||
|
const res3 = await agent.resource('departments').create({
|
||||||
|
values: {
|
||||||
|
title: 'Sub Department',
|
||||||
|
parent: dept1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const subDept = res3.body.data;
|
||||||
|
await agent.resource('departments').update({
|
||||||
|
filterByTk: subDept.id,
|
||||||
|
values: {
|
||||||
|
parent: dept2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const record1 = await repo.findOne({
|
||||||
|
filterByTk: dept1.id,
|
||||||
|
});
|
||||||
|
expect(record1.isLeaf).toBe(true);
|
||||||
|
const record2 = await repo.findOne({
|
||||||
|
filterByTk: dept2.id,
|
||||||
|
});
|
||||||
|
expect(record2.isLeaf).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
import { DepartmentModel } from '../models/department';
|
||||||
|
|
||||||
|
export const getAppendsOwners = async (ctx: Context, next: Next) => {
|
||||||
|
const { filterByTk, appends } = ctx.action.params;
|
||||||
|
const repo = ctx.db.getRepository('departments');
|
||||||
|
const department: DepartmentModel = await repo.findOne({
|
||||||
|
filterByTk,
|
||||||
|
appends,
|
||||||
|
});
|
||||||
|
const owners = await department.getOwners();
|
||||||
|
department.setDataValue('owners', owners);
|
||||||
|
ctx.body = department;
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const aggregateSearch = async (ctx: Context, next: Next) => {
|
||||||
|
const { keyword, type, last = 0, limit = 10 } = ctx.action.params.values || {};
|
||||||
|
let users = [];
|
||||||
|
let departments = [];
|
||||||
|
if (!type || type === 'user') {
|
||||||
|
const repo = ctx.db.getRepository('users');
|
||||||
|
users = await repo.find({
|
||||||
|
filter: {
|
||||||
|
id: { $gt: last },
|
||||||
|
$or: [
|
||||||
|
{ username: { $includes: keyword } },
|
||||||
|
{ nickname: { $includes: keyword } },
|
||||||
|
{ phone: { $includes: keyword } },
|
||||||
|
{ email: { $includes: keyword } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!type || type === 'department') {
|
||||||
|
const repo = ctx.db.getRepository('departments');
|
||||||
|
departments = await repo.find({
|
||||||
|
filter: {
|
||||||
|
id: { $gt: last },
|
||||||
|
title: { $includes: keyword },
|
||||||
|
},
|
||||||
|
appends: ['parent(recursively=true)'],
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.body = { users, departments };
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setOwner = async (ctx: Context, next: Next) => {
|
||||||
|
const { userId, departmentId } = ctx.action.params.values || {};
|
||||||
|
const throughRepo = ctx.db.getRepository('departmentsUsers');
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId,
|
||||||
|
departmentId,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isOwner: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeOwner = async (ctx: Context, next: Next) => {
|
||||||
|
const { userId, departmentId } = ctx.action.params.values || {};
|
||||||
|
const throughRepo = ctx.db.getRepository('departmentsUsers');
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId,
|
||||||
|
departmentId,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isOwner: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await next();
|
||||||
|
};
|
@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context, DEFAULT_PAGE, DEFAULT_PER_PAGE, Next } from '@nocobase/actions';
|
||||||
|
|
||||||
|
export const listExcludeDept = async (ctx: Context, next: Next) => {
|
||||||
|
const { departmentId, page = DEFAULT_PAGE, pageSize = DEFAULT_PER_PAGE } = ctx.action.params;
|
||||||
|
const repo = ctx.db.getRepository('users');
|
||||||
|
const members = await repo.find({
|
||||||
|
fields: ['id'],
|
||||||
|
filter: {
|
||||||
|
'departments.id': departmentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const memberIds = members.map((member: { id: number }) => member.id);
|
||||||
|
if (memberIds.length) {
|
||||||
|
ctx.action.mergeParams({
|
||||||
|
filter: {
|
||||||
|
id: {
|
||||||
|
$notIn: memberIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { filter } = ctx.action.params;
|
||||||
|
const [rows, count] = await repo.findAndCount({
|
||||||
|
context: ctx,
|
||||||
|
offset: (page - 1) * pageSize,
|
||||||
|
limit: +pageSize,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
ctx.body = {
|
||||||
|
count,
|
||||||
|
rows,
|
||||||
|
page: Number(page),
|
||||||
|
pageSize: Number(pageSize),
|
||||||
|
totalPage: Math.ceil(count / pageSize),
|
||||||
|
};
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setDepartments = async (ctx: Context, next: Next) => {
|
||||||
|
const { values = {} } = ctx.action.params;
|
||||||
|
const { userId, departments = [] } = values;
|
||||||
|
const repo = ctx.db.getRepository('users');
|
||||||
|
const throughRepo = ctx.db.getRepository('departmentsUsers');
|
||||||
|
const user = await repo.findOne({ filterByTk: userId });
|
||||||
|
if (!user) {
|
||||||
|
ctx.throw(400, ctx.t('User does not exist'));
|
||||||
|
}
|
||||||
|
const departmentIds = departments.map((department: any) => department.id);
|
||||||
|
const main = departments.find((department: any) => department.isMain);
|
||||||
|
const owners = departments.filter((department: any) => department.isOwner);
|
||||||
|
await ctx.db.sequelize.transaction(async (t) => {
|
||||||
|
await user.setDepartments(departmentIds, {
|
||||||
|
through: {
|
||||||
|
isMain: false,
|
||||||
|
isOwner: false,
|
||||||
|
},
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
if (main) {
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId,
|
||||||
|
departmentId: main.id,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (owners.length) {
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId,
|
||||||
|
departmentId: {
|
||||||
|
$in: owners.map((owner: any) => owner.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isOwner: true,
|
||||||
|
},
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setMainDepartment = async (ctx: Context, next: Next) => {
|
||||||
|
const { userId, departmentId } = ctx.action.params.values || {};
|
||||||
|
const throughRepo = ctx.db.getRepository('departmentsUsers');
|
||||||
|
await ctx.db.sequelize.transaction(async (t) => {
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId,
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isMain: false,
|
||||||
|
},
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId,
|
||||||
|
departmentId,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await next();
|
||||||
|
};
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineCollection } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default defineCollection({
|
||||||
|
name: 'departmentsRoles',
|
||||||
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite'],
|
||||||
|
});
|
@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineCollection } from '@nocobase/database';
|
||||||
|
|
||||||
|
export const ownersField = {
|
||||||
|
interface: 'm2m',
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'owners',
|
||||||
|
collectionName: 'departments',
|
||||||
|
target: 'users',
|
||||||
|
through: 'departmentsUsers',
|
||||||
|
foreignKey: 'departmentId',
|
||||||
|
otherKey: 'userId',
|
||||||
|
targetKey: 'id',
|
||||||
|
sourceKey: 'id',
|
||||||
|
throughScope: {
|
||||||
|
isOwner: true,
|
||||||
|
},
|
||||||
|
uiSchema: {
|
||||||
|
type: 'm2m',
|
||||||
|
title: '{{t("Owners")}}',
|
||||||
|
'x-component': 'DepartmentOwnersField',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: true,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'nickname',
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineCollection({
|
||||||
|
name: 'departments',
|
||||||
|
migrationRules: ['overwrite'],
|
||||||
|
title: '{{t("Departments")}}',
|
||||||
|
dumpRules: 'required',
|
||||||
|
tree: 'adjacency-list',
|
||||||
|
template: 'tree',
|
||||||
|
shared: true,
|
||||||
|
sortable: true,
|
||||||
|
model: 'DepartmentModel',
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
logging: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'bigInt',
|
||||||
|
name: 'id',
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
interface: 'id',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("ID")}}',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'title',
|
||||||
|
interface: 'input',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Department name")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'isLeaf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
target: 'departments',
|
||||||
|
foreignKey: 'parentId',
|
||||||
|
treeParent: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
interface: 'm2o',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'm2o',
|
||||||
|
title: '{{t("Superior department")}}',
|
||||||
|
'x-component': 'AssociationField',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: false,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'title',
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children',
|
||||||
|
target: 'departments',
|
||||||
|
foreignKey: 'parentId',
|
||||||
|
treeChildren: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'members',
|
||||||
|
target: 'users',
|
||||||
|
through: 'departmentsUsers',
|
||||||
|
foreignKey: 'departmentId',
|
||||||
|
otherKey: 'userId',
|
||||||
|
targetKey: 'id',
|
||||||
|
sourceKey: 'id',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'm2m',
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'roles',
|
||||||
|
target: 'roles',
|
||||||
|
through: 'departmentsRoles',
|
||||||
|
foreignKey: 'departmentId',
|
||||||
|
otherKey: 'roleName',
|
||||||
|
targetKey: 'name',
|
||||||
|
sourceKey: 'id',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'm2m',
|
||||||
|
title: '{{t("Roles")}}',
|
||||||
|
'x-component': 'AssociationField',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: true,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'title',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ownersField,
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineCollection } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default defineCollection({
|
||||||
|
name: 'departmentsUsers',
|
||||||
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['schema-only'],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'isOwner', // Weather the user is the owner of the department
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'isMain', // Weather this is the main department of the user
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extendCollection } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default extendCollection({
|
||||||
|
name: 'roles',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'departments',
|
||||||
|
target: 'departments',
|
||||||
|
foreignKey: 'roleName',
|
||||||
|
otherKey: 'departmentId',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
sourceKey: 'name',
|
||||||
|
targetKey: 'id',
|
||||||
|
through: 'departmentsRoles',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extendCollection } from '@nocobase/database';
|
||||||
|
|
||||||
|
export const departmentsField = {
|
||||||
|
collectionName: 'users',
|
||||||
|
interface: 'm2m',
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'departments',
|
||||||
|
target: 'departments',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
otherKey: 'departmentId',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
sourceKey: 'id',
|
||||||
|
targetKey: 'id',
|
||||||
|
through: 'departmentsUsers',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'm2m',
|
||||||
|
title: '{{t("Departments")}}',
|
||||||
|
'x-component': 'UserDepartmentsField',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: true,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'title',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mainDepartmentField = {
|
||||||
|
collectionName: 'users',
|
||||||
|
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: 'm2m',
|
||||||
|
title: '{{t("Main department")}}',
|
||||||
|
'x-component': 'UserMainDepartmentField',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: false,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'title',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default extendCollection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [departmentsField, mainDepartmentField],
|
||||||
|
});
|
@ -0,0 +1,333 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Model } from '@nocobase/database';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import {
|
||||||
|
FormatDepartment,
|
||||||
|
FormatUserDepartment,
|
||||||
|
OriginRecord,
|
||||||
|
PrimaryKey,
|
||||||
|
RecordResourceChanged,
|
||||||
|
SyncAccept,
|
||||||
|
UserDataResource,
|
||||||
|
} from '@nocobase/plugin-user-data-sync';
|
||||||
|
|
||||||
|
export class DepartmentDataSyncResource extends UserDataResource {
|
||||||
|
name = 'departments';
|
||||||
|
accepts: SyncAccept[] = ['user', 'department'];
|
||||||
|
|
||||||
|
get userRepo() {
|
||||||
|
return this.db.getRepository('users');
|
||||||
|
}
|
||||||
|
|
||||||
|
get deptRepo() {
|
||||||
|
return this.db.getRepository('departments');
|
||||||
|
}
|
||||||
|
|
||||||
|
get deptUserRepo() {
|
||||||
|
return this.db.getRepository('departmentsUsers');
|
||||||
|
}
|
||||||
|
|
||||||
|
getFlteredSourceDepartment(sourceDepartment: FormatDepartment) {
|
||||||
|
const deleteProps = [
|
||||||
|
'id',
|
||||||
|
'uid',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'sort',
|
||||||
|
'createdById',
|
||||||
|
'updatedById',
|
||||||
|
'isDeleted',
|
||||||
|
'parentId',
|
||||||
|
'parentUid',
|
||||||
|
];
|
||||||
|
return lodash.omit(sourceDepartment, deleteProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(record: OriginRecord, resourcePks: PrimaryKey[]): Promise<RecordResourceChanged[]> {
|
||||||
|
const { dataType, metaData, sourceName } = record;
|
||||||
|
if (dataType === 'user') {
|
||||||
|
const sourceUser = metaData;
|
||||||
|
if (sourceUser.isDeleted) {
|
||||||
|
if (!resourcePks || !resourcePks.length) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return resourcePks.map((id) => ({ resourcesPk: id, isDeleted: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const resources = record.resources.filter((r) => r.resource === 'users');
|
||||||
|
if (!resources.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const user = await this.userRepo.findOne({
|
||||||
|
filterByTk: resources[0].resourcePk,
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
if (!resourcePks || !resourcePks.length) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return resourcePks.map((id) => ({ resourcesPk: id, isDeleted: true }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return await this.updateUserDepartments(user, resourcePks, sourceUser.departments, sourceName);
|
||||||
|
}
|
||||||
|
} else if (dataType === 'department') {
|
||||||
|
const sourceDepartment = metaData;
|
||||||
|
const department = await this.deptRepo.findOne({
|
||||||
|
filterByTk: resourcePks[0],
|
||||||
|
});
|
||||||
|
if (!department) {
|
||||||
|
if (sourceDepartment.isDeleted) {
|
||||||
|
return [{ resourcesPk: resourcePks[0], isDeleted: true }];
|
||||||
|
}
|
||||||
|
const result = await this.create(record);
|
||||||
|
return [...result, { resourcesPk: resourcePks[0], isDeleted: true }];
|
||||||
|
}
|
||||||
|
await this.updateDepartment(department, sourceDepartment, sourceName);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`update department: unsupported data type: ${dataType}`);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(record: OriginRecord): Promise<RecordResourceChanged[]> {
|
||||||
|
const { dataType, metaData, sourceName } = record;
|
||||||
|
if (dataType === 'user') {
|
||||||
|
const sourceUser = metaData;
|
||||||
|
if (sourceUser.isDeleted) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const resources = record.resources.filter((r) => r.resource === 'users');
|
||||||
|
if (!resources.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const user = await this.userRepo.findOne({
|
||||||
|
filterByTk: resources[0].resourcePk,
|
||||||
|
});
|
||||||
|
return await this.updateUserDepartments(user, [], sourceUser.departments, sourceName);
|
||||||
|
} else if (dataType === 'department') {
|
||||||
|
const sourceDepartment = metaData;
|
||||||
|
const newDepartmentId = await this.createDepartment(sourceDepartment, sourceName);
|
||||||
|
return [{ resourcesPk: newDepartmentId, isDeleted: false }];
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`create department: unsupported data type: ${dataType}`);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDepartmentIdsBySourceUks(sourceUks: PrimaryKey[], sourceName: string) {
|
||||||
|
const syncDepartmentRecords = await this.syncRecordRepo.find({
|
||||||
|
filter: {
|
||||||
|
sourceName,
|
||||||
|
dataType: 'department',
|
||||||
|
sourceUk: { $in: sourceUks },
|
||||||
|
'resources.resource': this.name,
|
||||||
|
},
|
||||||
|
appends: ['resources'],
|
||||||
|
});
|
||||||
|
const departmentIds = syncDepartmentRecords
|
||||||
|
.filter((record) => record.resources?.length)
|
||||||
|
.map((record) => record.resources[0].resourcePk);
|
||||||
|
return departmentIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDepartmentIdBySourceUk(sourceUk: PrimaryKey, sourceName: string) {
|
||||||
|
const syncDepartmentRecord = await this.syncRecordRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
sourceName,
|
||||||
|
dataType: 'department',
|
||||||
|
sourceUk,
|
||||||
|
'resources.resource': this.name,
|
||||||
|
},
|
||||||
|
appends: ['resources'],
|
||||||
|
});
|
||||||
|
if (syncDepartmentRecord && syncDepartmentRecord.resources?.length) {
|
||||||
|
return syncDepartmentRecord.resources[0].resourcePk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserDepartments(
|
||||||
|
user: any,
|
||||||
|
currentDepartmentIds: PrimaryKey[],
|
||||||
|
sourceDepartments: (PrimaryKey | FormatUserDepartment)[],
|
||||||
|
sourceName: string,
|
||||||
|
): Promise<RecordResourceChanged[]> {
|
||||||
|
if (!this.deptRepo) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!sourceDepartments || !sourceDepartments.length) {
|
||||||
|
const userDepartments = await user.getDepartments();
|
||||||
|
if (userDepartments.length) {
|
||||||
|
await user.removeDepartments(userDepartments);
|
||||||
|
}
|
||||||
|
if (currentDepartmentIds && currentDepartmentIds.length) {
|
||||||
|
return currentDepartmentIds.map((id) => ({ resourcesPk: id, isDeleted: true }));
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const sourceDepartmentIds = sourceDepartments.map((sourceDepartment) => {
|
||||||
|
if (typeof sourceDepartment === 'string' || typeof sourceDepartment === 'number') {
|
||||||
|
return sourceDepartment;
|
||||||
|
}
|
||||||
|
return sourceDepartment.uid;
|
||||||
|
});
|
||||||
|
const newDepartmentIds = await this.getDepartmentIdsBySourceUks(sourceDepartmentIds, sourceName);
|
||||||
|
const newDepartments = await this.deptRepo.find({
|
||||||
|
filter: { id: { $in: newDepartmentIds } },
|
||||||
|
});
|
||||||
|
const realCurrentDepartments = await user.getDepartments();
|
||||||
|
// 需要删除的部门
|
||||||
|
const toRealRemoveDepartments = realCurrentDepartments.filter((currnetDepartment) => {
|
||||||
|
return !newDepartments.find((newDepartment) => newDepartment.id === currnetDepartment.id);
|
||||||
|
});
|
||||||
|
if (toRealRemoveDepartments.length) {
|
||||||
|
await user.removeDepartments(toRealRemoveDepartments);
|
||||||
|
}
|
||||||
|
// 需要添加的部门
|
||||||
|
const toRealAddDepartments = newDepartments.filter((newDepartment) => {
|
||||||
|
if (realCurrentDepartments.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !realCurrentDepartments.find((currentDepartment) => currentDepartment.id === newDepartment.id);
|
||||||
|
});
|
||||||
|
if (toRealAddDepartments.length) {
|
||||||
|
await user.addDepartments(toRealAddDepartments);
|
||||||
|
}
|
||||||
|
// 更新部门主管和主部门
|
||||||
|
for (const sourceDepartment of sourceDepartments) {
|
||||||
|
this.logger.debug('update dept owner: ' + JSON.stringify(sourceDepartment));
|
||||||
|
let isOwner = false;
|
||||||
|
let isMain = false;
|
||||||
|
let uid;
|
||||||
|
if (typeof sourceDepartment !== 'string' && typeof sourceDepartment !== 'number') {
|
||||||
|
isOwner = sourceDepartment.isOwner || false;
|
||||||
|
isMain = sourceDepartment.isMain || false;
|
||||||
|
uid = sourceDepartment.uid;
|
||||||
|
} else {
|
||||||
|
uid = sourceDepartment;
|
||||||
|
}
|
||||||
|
const deptId = await this.getDepartmentIdBySourceUk(uid, sourceName);
|
||||||
|
this.logger.debug('update dept owner: ' + JSON.stringify({ deptId, isOwner, isMain, userId: user.id }));
|
||||||
|
if (!deptId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await this.deptUserRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId: user.id,
|
||||||
|
departmentId: deptId,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isOwner,
|
||||||
|
isMain,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const recordResourceChangeds: RecordResourceChanged[] = [];
|
||||||
|
if (currentDepartmentIds !== undefined && currentDepartmentIds.length > 0) {
|
||||||
|
// 需要删除的部门ID
|
||||||
|
const toRemoveDepartmentIds = currentDepartmentIds.filter(
|
||||||
|
(currentDepartmentId) => !newDepartmentIds.includes(currentDepartmentId),
|
||||||
|
);
|
||||||
|
recordResourceChangeds.push(
|
||||||
|
...toRemoveDepartmentIds.map((departmentId) => {
|
||||||
|
return { resourcesPk: departmentId, isDeleted: true };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// 需要添加的部门ID
|
||||||
|
const toAddDepartmentIds = newDepartmentIds.filter(
|
||||||
|
(newDepartmentId) => !currentDepartmentIds.includes(newDepartmentId),
|
||||||
|
);
|
||||||
|
recordResourceChangeds.push(
|
||||||
|
...toAddDepartmentIds.map((departmentId) => {
|
||||||
|
return { resourcesPk: departmentId, isDeleted: false };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
recordResourceChangeds.push(
|
||||||
|
...toRealAddDepartments.map((department) => {
|
||||||
|
return {
|
||||||
|
resourcesPk: department.id,
|
||||||
|
isDeleted: false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return recordResourceChangeds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDepartment(department: Model, sourceDepartment: FormatDepartment, sourceName: string) {
|
||||||
|
if (sourceDepartment.isDeleted) {
|
||||||
|
// 删除部门
|
||||||
|
await department.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dataChanged = false;
|
||||||
|
const filteredSourceDepartment = this.getFlteredSourceDepartment(sourceDepartment);
|
||||||
|
lodash.forOwn(filteredSourceDepartment, (value, key) => {
|
||||||
|
if (department[key] !== value) {
|
||||||
|
department[key] = value;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (dataChanged) {
|
||||||
|
await department.save();
|
||||||
|
}
|
||||||
|
await this.updateParentDepartment(department, sourceDepartment.parentUid, sourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDepartment(sourceDepartment: FormatDepartment, sourceName: string): Promise<string> {
|
||||||
|
const filteredSourceDepartment = this.getFlteredSourceDepartment(sourceDepartment);
|
||||||
|
const department = await this.deptRepo.create({
|
||||||
|
values: filteredSourceDepartment,
|
||||||
|
});
|
||||||
|
await this.updateParentDepartment(department, sourceDepartment.parentUid, sourceName);
|
||||||
|
return department.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateParentDepartment(department: Model, parentUid: string, sourceName: string) {
|
||||||
|
if (!parentUid) {
|
||||||
|
const parentDepartment = await department.getParent();
|
||||||
|
if (parentDepartment) {
|
||||||
|
await department.setParent(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const syncDepartmentRecord = await this.syncRecordRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
sourceName,
|
||||||
|
dataType: 'department',
|
||||||
|
sourceUk: parentUid,
|
||||||
|
'resources.resource': this.name,
|
||||||
|
},
|
||||||
|
appends: ['resources'],
|
||||||
|
});
|
||||||
|
if (syncDepartmentRecord && syncDepartmentRecord.resources?.length) {
|
||||||
|
const parentDepartment = await this.deptRepo.findOne({
|
||||||
|
filterByTk: syncDepartmentRecord.resources[0].resourcePk,
|
||||||
|
});
|
||||||
|
if (!parentDepartment) {
|
||||||
|
await department.setParent(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parent = await department.getParent();
|
||||||
|
if (parent) {
|
||||||
|
if (parentDepartment.id !== parent.id) {
|
||||||
|
await department.setParent(parentDepartment);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await department.setParent(parentDepartment);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await department.setParent(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default } from './plugin';
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
|
||||||
|
const destroyCheck = async (ctx: Context) => {
|
||||||
|
const { filterByTk } = ctx.action.params;
|
||||||
|
const repo = ctx.db.getRepository('departments');
|
||||||
|
const children = await repo.count({
|
||||||
|
filter: {
|
||||||
|
parentId: filterByTk,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (children) {
|
||||||
|
ctx.throw(400, ctx.t('The department has sub-departments, please delete them first', { ns: 'departments' }));
|
||||||
|
}
|
||||||
|
const members = await ctx.db.getRepository('departmentsUsers').count({
|
||||||
|
filter: {
|
||||||
|
departmentId: filterByTk,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (members) {
|
||||||
|
ctx.throw(400, ctx.t('The department has members, please remove them first', { ns: 'departments' }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const destroyDepartmentCheck = async (ctx: Context, next: Next) => {
|
||||||
|
const { resourceName, actionName } = ctx.action.params;
|
||||||
|
if (resourceName === 'departments' && actionName === 'destroy') {
|
||||||
|
await destroyCheck(ctx as any);
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './destroy-department-check';
|
||||||
|
export * from './reset-user-departments-cache';
|
||||||
|
export * from './set-department-owners';
|
||||||
|
export * from './update-department-isleaf';
|
||||||
|
export * from './set-departments-roles';
|
||||||
|
export * from './set-main-department';
|
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
import { Cache } from '@nocobase/cache';
|
||||||
|
|
||||||
|
export const resetUserDepartmentsCache = async (ctx: Context, next: Next) => {
|
||||||
|
await next();
|
||||||
|
const { associatedName, resourceName, associatedIndex, actionName, values } = ctx.action.params;
|
||||||
|
const cache = ctx.app.cache as Cache;
|
||||||
|
if (
|
||||||
|
associatedName === 'departments' &&
|
||||||
|
resourceName === 'members' &&
|
||||||
|
['add', 'remove', 'set'].includes(actionName) &&
|
||||||
|
values?.length
|
||||||
|
) {
|
||||||
|
// Delete cache when the members of a department changed
|
||||||
|
for (const memberId of values) {
|
||||||
|
await cache.del(`departments:${memberId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associatedName === 'users' && resourceName === 'departments' && ['add', 'remove', 'set'].includes(actionName)) {
|
||||||
|
await cache.del(`departments:${associatedIndex}`);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
const setOwners = async (ctx: Context, filterByTk: any, owners: any[]) => {
|
||||||
|
const throughRepo = ctx.db.getRepository('departmentsUsers');
|
||||||
|
await ctx.db.sequelize.transaction(async (t) => {
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
departmentId: filterByTk,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isOwner: false,
|
||||||
|
},
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
departmentId: filterByTk,
|
||||||
|
userId: {
|
||||||
|
$in: owners.map((owner: any) => owner.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isOwner: true,
|
||||||
|
},
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setDepartmentOwners = async (ctx: Context, next: Next) => {
|
||||||
|
const { filterByTk, values = {}, resourceName, actionName } = ctx.action.params;
|
||||||
|
const { owners } = values;
|
||||||
|
if (resourceName === 'departments' && actionName === 'update' && owners) {
|
||||||
|
ctx.action.params.values = lodash.omit(values, ['owners']);
|
||||||
|
await next();
|
||||||
|
await setOwners(ctx as any, filterByTk, owners);
|
||||||
|
} else {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
import { Cache } from '@nocobase/cache';
|
||||||
|
import { Model, Repository } from '@nocobase/database';
|
||||||
|
|
||||||
|
export const setDepartmentsInfo = async (ctx: Context, next: Next) => {
|
||||||
|
const currentUser = ctx.state.currentUser;
|
||||||
|
if (!currentUser) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = ctx.cache as Cache;
|
||||||
|
const repo = ctx.db.getRepository('users.departments', currentUser.id) as unknown as Repository;
|
||||||
|
const departments = (await cache.wrap(`departments:${currentUser.id}`, () =>
|
||||||
|
repo.find({
|
||||||
|
appends: ['owners', 'roles', 'parent(recursively=true)'],
|
||||||
|
raw: true,
|
||||||
|
}),
|
||||||
|
)) as Model[];
|
||||||
|
if (!departments.length) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
ctx.state.currentUser.departments = departments;
|
||||||
|
ctx.state.currentUser.mainDeparmtent = departments.find((dept) => dept.isMain);
|
||||||
|
|
||||||
|
const departmentIds = departments.map((dept) => dept.id);
|
||||||
|
const roleRepo = ctx.db.getRepository('roles');
|
||||||
|
const roles = await roleRepo.find({
|
||||||
|
filter: {
|
||||||
|
'departments.id': {
|
||||||
|
$in: departmentIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!roles.length) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
const rolesMap = new Map();
|
||||||
|
roles.forEach((role: any) => rolesMap.set(role.name, role));
|
||||||
|
ctx.state.attachRoles = Array.from(rolesMap.values());
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
|
||||||
|
export const setMainDepartment = async (ctx: Context, next: Next) => {
|
||||||
|
await next();
|
||||||
|
const { associatedName, resourceName, associatedIndex, actionName, values } = ctx.action.params;
|
||||||
|
if (associatedName === 'departments' && resourceName === 'members' && values?.length) {
|
||||||
|
const throughRepo = ctx.db.getRepository('departmentsUsers');
|
||||||
|
const usersHasMain = await throughRepo.find({
|
||||||
|
filter: {
|
||||||
|
userId: {
|
||||||
|
$in: values,
|
||||||
|
},
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const userIdsHasMain = usersHasMain.map((item) => item.userId);
|
||||||
|
if (actionName === 'add' || actionName === 'set') {
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId: {
|
||||||
|
$in: values.filter((id) => !userIdsHasMain.includes(id)),
|
||||||
|
},
|
||||||
|
departmentId: associatedIndex,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionName === 'remove') {
|
||||||
|
const userIdsHasNoMain = values.filter((id) => !userIdsHasMain.includes(id));
|
||||||
|
for (const userId of userIdsHasNoMain) {
|
||||||
|
const firstDept = await throughRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (firstDept) {
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId,
|
||||||
|
departmentId: firstDept.departmentId,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (associatedName === 'users' && resourceName === 'departments' && ['add', 'remove', 'set'].includes(actionName)) {
|
||||||
|
const throughRepo = ctx.db.getRepository('departmentsUsers');
|
||||||
|
const hasMain = await throughRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
userId: associatedIndex,
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (hasMain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const firstDept = await throughRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
userId: associatedIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (firstDept) {
|
||||||
|
await throughRepo.update({
|
||||||
|
filter: {
|
||||||
|
userId: associatedIndex,
|
||||||
|
departmentId: firstDept.departmentId,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isMain: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
import { Repository } from '@nocobase/database';
|
||||||
|
|
||||||
|
const updateIsLeafWhenAddChild = async (repo: Repository, parent: any) => {
|
||||||
|
if (parent && parent.isLeaf !== false) {
|
||||||
|
await repo.update({
|
||||||
|
filter: {
|
||||||
|
id: parent.id,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isLeaf: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateIsLeafWhenChangeChild = async (
|
||||||
|
repo: Repository,
|
||||||
|
oldParentId: number | null,
|
||||||
|
newParentId: number | null,
|
||||||
|
) => {
|
||||||
|
if (oldParentId && oldParentId !== newParentId) {
|
||||||
|
const hasChild = await repo.count({
|
||||||
|
filter: {
|
||||||
|
parentId: oldParentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!hasChild) {
|
||||||
|
await repo.update({
|
||||||
|
filter: {
|
||||||
|
id: oldParentId,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDepartmentIsLeaf = async (ctx: Context, next: Next) => {
|
||||||
|
const { filterByTk, values = {}, resourceName, actionName } = ctx.action.params;
|
||||||
|
const repo = ctx.db.getRepository('departments');
|
||||||
|
const { parent } = values;
|
||||||
|
if (resourceName === 'departments' && actionName === 'create') {
|
||||||
|
ctx.action.params.values = { ...values, isLeaf: true };
|
||||||
|
await next();
|
||||||
|
await updateIsLeafWhenAddChild(repo, parent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceName === 'departments' && actionName === 'update') {
|
||||||
|
const department = await repo.findOne({ filterByTk });
|
||||||
|
await next();
|
||||||
|
await Promise.all([
|
||||||
|
updateIsLeafWhenChangeChild(repo, department.parentId, parent?.id),
|
||||||
|
updateIsLeafWhenAddChild(repo, parent),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceName === 'departments' && actionName === 'destroy') {
|
||||||
|
const department = await repo.findOne({ filterByTk });
|
||||||
|
await next();
|
||||||
|
await updateIsLeafWhenChangeChild(repo, department.parentId, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Migration } from '@nocobase/server';
|
||||||
|
import { departmentsField, mainDepartmentField } from '../collections/users';
|
||||||
|
import { ownersField } from '../collections/departments';
|
||||||
|
|
||||||
|
export default class UpdateFieldUISchemasMigration extends Migration {
|
||||||
|
async up() {
|
||||||
|
const result = await this.app.version.satisfies('<=0.20.0-alpha.6');
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldRepo = this.db.getRepository('fields');
|
||||||
|
const departmentsFieldInstance = await fieldRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'departments',
|
||||||
|
collectionName: 'users',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (departmentsFieldInstance) {
|
||||||
|
const options = {
|
||||||
|
...departmentsFieldInstance.options,
|
||||||
|
uiSchema: departmentsField.uiSchema,
|
||||||
|
};
|
||||||
|
await fieldRepo.update({
|
||||||
|
filter: {
|
||||||
|
name: 'departments',
|
||||||
|
collectionName: 'users',
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const mainDepartmentFieldInstance = await fieldRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'mainDepartment',
|
||||||
|
collectionName: 'users',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (mainDepartmentFieldInstance) {
|
||||||
|
const options = {
|
||||||
|
...mainDepartmentFieldInstance.options,
|
||||||
|
uiSchema: mainDepartmentField.uiSchema,
|
||||||
|
};
|
||||||
|
await fieldRepo.update({
|
||||||
|
filter: {
|
||||||
|
name: 'mainDepartment',
|
||||||
|
collectionName: 'users',
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const ownersFieldInstance = await fieldRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'owners',
|
||||||
|
collectionName: 'departments',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (ownersFieldInstance) {
|
||||||
|
const options = {
|
||||||
|
...ownersFieldInstance.options,
|
||||||
|
uiSchema: ownersField.uiSchema,
|
||||||
|
};
|
||||||
|
await fieldRepo.update({
|
||||||
|
filter: {
|
||||||
|
name: 'owners',
|
||||||
|
collectionName: 'departments',
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Model } from '@nocobase/database';
|
||||||
|
|
||||||
|
export class DepartmentModel extends Model {
|
||||||
|
getOwners() {
|
||||||
|
return this.getMembers({
|
||||||
|
through: {
|
||||||
|
where: {
|
||||||
|
isOwner: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* 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 <https://www.nocobase.com/agreement>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Cache } from '@nocobase/cache';
|
||||||
|
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||||
|
import { aggregateSearch, removeOwner, setOwner } from './actions/departments';
|
||||||
|
import { listExcludeDept, setMainDepartment } from './actions/users';
|
||||||
|
import { departmentsField, mainDepartmentField } from './collections/users';
|
||||||
|
import {
|
||||||
|
destroyDepartmentCheck,
|
||||||
|
resetUserDepartmentsCache,
|
||||||
|
setDepartmentOwners,
|
||||||
|
setMainDepartment as setMainDepartmentMiddleware,
|
||||||
|
updateDepartmentIsLeaf,
|
||||||
|
} from './middlewares';
|
||||||
|
import { setDepartmentsInfo } from './middlewares/set-departments-roles';
|
||||||
|
import { DepartmentModel } from './models/department';
|
||||||
|
import { DepartmentDataSyncResource } from './department-data-sync-resource';
|
||||||
|
import PluginUserDataSyncServer from '@nocobase/plugin-user-data-sync';
|
||||||
|
import { DataSource } from '@nocobase/data-source-manager';
|
||||||
|
|
||||||
|
export class PluginDepartmentsServer extends Plugin {
|
||||||
|
afterAdd() {}
|
||||||
|
|
||||||
|
beforeLoad() {
|
||||||
|
this.app.db.registerModels({ DepartmentModel });
|
||||||
|
|
||||||
|
this.app.acl.addFixedParams('collections', 'destroy', () => {
|
||||||
|
return {
|
||||||
|
filter: {
|
||||||
|
'name.$notIn': ['departments', 'departmentsUsers', 'departmentsRoles'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.app.resourceManager.registerActionHandlers({
|
||||||
|
'users:listExcludeDept': listExcludeDept,
|
||||||
|
'users:setMainDepartment': setMainDepartment,
|
||||||
|
'departments:aggregateSearch': aggregateSearch,
|
||||||
|
'departments:setOwner': setOwner,
|
||||||
|
'departments:removeOwner': removeOwner,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.acl.allow('users', ['setMainDepartment', 'listExcludeDept'], 'loggedIn');
|
||||||
|
this.app.acl.registerSnippet({
|
||||||
|
name: `pm.${this.name}`,
|
||||||
|
actions: [
|
||||||
|
'departments:*',
|
||||||
|
'roles:list',
|
||||||
|
'users:list',
|
||||||
|
'users:listExcludeDept',
|
||||||
|
'users:setMainDepartment',
|
||||||
|
'users.departments:*',
|
||||||
|
'roles.departments:*',
|
||||||
|
'departments.members:*',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.resourceManager.use(setDepartmentsInfo, {
|
||||||
|
tag: 'setDepartmentsInfo',
|
||||||
|
before: 'setCurrentRole',
|
||||||
|
after: 'auth',
|
||||||
|
});
|
||||||
|
this.app.dataSourceManager.afterAddDataSource((dataSource: DataSource) => {
|
||||||
|
dataSource.resourceManager.use(setDepartmentsInfo, {
|
||||||
|
tag: 'setDepartmentsInfo',
|
||||||
|
before: 'setCurrentRole',
|
||||||
|
after: 'auth',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.resourceManager.use(setDepartmentOwners);
|
||||||
|
this.app.resourceManager.use(destroyDepartmentCheck);
|
||||||
|
this.app.resourceManager.use(updateDepartmentIsLeaf);
|
||||||
|
this.app.resourceManager.use(resetUserDepartmentsCache);
|
||||||
|
this.app.resourceManager.use(setMainDepartmentMiddleware);
|
||||||
|
|
||||||
|
// Delete cache when the departments of a user changed
|
||||||
|
this.app.db.on('departmentsUsers.afterSave', async (model) => {
|
||||||
|
const cache = this.app.cache as Cache;
|
||||||
|
await cache.del(`departments:${model.get('userId')}`);
|
||||||
|
});
|
||||||
|
this.app.db.on('departmentsUsers.afterDestroy', async (model) => {
|
||||||
|
const cache = this.app.cache as Cache;
|
||||||
|
await cache.del(`departments:${model.get('userId')}`);
|
||||||
|
});
|
||||||
|
this.app.on('beforeSignOut', ({ userId }) => {
|
||||||
|
this.app.cache.del(`departments:${userId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const userDataSyncPlugin = this.app.pm.get('user-data-sync') as PluginUserDataSyncServer;
|
||||||
|
if (userDataSyncPlugin && userDataSyncPlugin.enabled) {
|
||||||
|
userDataSyncPlugin.resourceManager.registerResource(new DepartmentDataSyncResource(this.db, this.app.logger), {
|
||||||
|
// write department records after writing user records
|
||||||
|
after: 'users',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async install(options?: InstallOptions) {
|
||||||
|
const collectionRepo = this.db.getRepository<any>('collections');
|
||||||
|
if (collectionRepo) {
|
||||||
|
await collectionRepo.db2cm('departments');
|
||||||
|
}
|
||||||
|
const fieldRepo = this.db.getRepository('fields');
|
||||||
|
if (fieldRepo) {
|
||||||
|
const isDepartmentsFieldExists = await fieldRepo.count({
|
||||||
|
filter: {
|
||||||
|
name: 'departments',
|
||||||
|
collectionName: 'users',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!isDepartmentsFieldExists) {
|
||||||
|
await fieldRepo.create({
|
||||||
|
values: departmentsField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const isMainDepartmentFieldExists = await fieldRepo.count({
|
||||||
|
filter: {
|
||||||
|
name: 'mainDepartment',
|
||||||
|
collectionName: 'users',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!isMainDepartmentFieldExists) {
|
||||||
|
await fieldRepo.create({
|
||||||
|
values: mainDepartmentField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterEnable() {}
|
||||||
|
|
||||||
|
async afterDisable() {}
|
||||||
|
|
||||||
|
async remove() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginDepartmentsServer;
|
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
@ -0,0 +1 @@
|
|||||||
|
# @nocobase/plugin-field-attachment-url
|
2
packages/plugins/@nocobase/plugin-field-attachment-url/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-field-attachment-url/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/client';
|
||||||
|
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client/index.js');
|
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@nocobase/plugin-field-attachment-url",
|
||||||
|
"version": "1.6.19",
|
||||||
|
"main": "dist/server/index.js",
|
||||||
|
"displayName": "Collection field: Attachment(URL)",
|
||||||
|
"displayName.zh-CN": "数据表字段:附件(URL)",
|
||||||
|
"description": "Supports attachments in URL format.",
|
||||||
|
"description.zh-CN": "支持 URL 格式的附件。",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nocobase/client": "1.x",
|
||||||
|
"@nocobase/server": "1.x",
|
||||||
|
"@nocobase/test": "1.x"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nocobase/plugin-file-manager": "1.x"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Collection fields"
|
||||||
|
]
|
||||||
|
}
|
2
packages/plugins/@nocobase/plugin-field-attachment-url/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-field-attachment-url/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/server';
|
||||||
|
export { default } from './dist/server';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/server/index.js');
|
@ -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 { expect, test } from '@nocobase/test/e2e';
|
||||||
|
|
||||||
|
test('create Attachment (URL) field', async ({ page }) => {
|
||||||
|
await page.goto('/admin/settings/data-source-manager/main/collections?type=main');
|
||||||
|
await page.getByLabel('action-Action.Link-Configure fields-collections-users', { exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'plus Add field' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Attachment (URL)' }).click();
|
||||||
|
const displayName = `a${Math.random().toString(36).substring(7)}`;
|
||||||
|
const name = `a${Math.random().toString(36).substring(7)}`;
|
||||||
|
await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').fill(displayName);
|
||||||
|
await page.getByLabel('block-item-Input-fields-Field name').getByRole('textbox').fill(name);
|
||||||
|
await expect(page.getByLabel('block-item-RemoteSelect-')).toBeVisible();
|
||||||
|
await page.getByLabel('action-Action-Submit-fields-').click();
|
||||||
|
await expect(page.getByText(name)).toBeVisible();
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
await page.getByLabel(`action-CollectionFields-Delete-fields-${name}`).click();
|
||||||
|
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||||
|
});
|
249
packages/plugins/@nocobase/plugin-field-attachment-url/src/client/client.d.ts
vendored
Normal file
249
packages/plugins/@nocobase/plugin-field-attachment-url/src/client/client.d.ts
vendored
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CSS modules
|
||||||
|
type CSSModuleClasses = { readonly [key: string]: string };
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sass' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.less' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.styl' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.stylus' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.pcss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS
|
||||||
|
declare module '*.css' { }
|
||||||
|
declare module '*.scss' { }
|
||||||
|
declare module '*.sass' { }
|
||||||
|
declare module '*.less' { }
|
||||||
|
declare module '*.styl' { }
|
||||||
|
declare module '*.stylus' { }
|
||||||
|
declare module '*.pcss' { }
|
||||||
|
declare module '*.sss' { }
|
||||||
|
|
||||||
|
// Built-in asset types
|
||||||
|
// see `src/node/constants.ts`
|
||||||
|
|
||||||
|
// images
|
||||||
|
declare module '*.apng' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jfif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ico' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.avif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// media
|
||||||
|
declare module '*.mp4' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webm' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ogg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mp3' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.wav' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.flac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.aac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.opus' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mov' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.m4a' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.vtt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fonts
|
||||||
|
declare module '*.woff' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.woff2' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.eot' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ttf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.otf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// other
|
||||||
|
declare module '*.webmanifest' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pdf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.txt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wasm?init
|
||||||
|
declare module '*.wasm?init' {
|
||||||
|
const initWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
|
||||||
|
export default initWasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// web worker
|
||||||
|
declare module '*?worker' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&inline' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&inline' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?raw' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?inline' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* 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 { RecursionField, connect, mapReadPretty, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
FormProvider,
|
||||||
|
RecordPickerContext,
|
||||||
|
RecordPickerProvider,
|
||||||
|
SchemaComponentOptions,
|
||||||
|
useActionContext,
|
||||||
|
TableSelectorParamsProvider,
|
||||||
|
useTableSelectorProps as useTsp,
|
||||||
|
EllipsisWithTooltip,
|
||||||
|
CollectionProvider_deprecated,
|
||||||
|
useCollection_deprecated,
|
||||||
|
useCollectionManager_deprecated,
|
||||||
|
Upload,
|
||||||
|
useFieldNames,
|
||||||
|
ActionContextProvider,
|
||||||
|
AssociationField,
|
||||||
|
Input,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import schema from '../schema';
|
||||||
|
import { useInsertSchema } from '../hook';
|
||||||
|
|
||||||
|
const defaultToValueItem = (data) => {
|
||||||
|
return data?.thumbnailRule ? `${data?.url}${data?.thumbnailRule}` : data?.url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InnerAttachmentUrl = (props) => {
|
||||||
|
const { value, onChange, toValueItem = defaultToValueItem, disabled, underFilter, ...others } = props;
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const [visibleSelector, setVisibleSelector] = useState(false);
|
||||||
|
const [selectedRows, setSelectedRows] = useState([]);
|
||||||
|
const insertSelector = useInsertSchema('Selector');
|
||||||
|
const fieldNames = useFieldNames(props);
|
||||||
|
const field: any = useField();
|
||||||
|
const [options, setOptions] = useState();
|
||||||
|
const { getField } = useCollection_deprecated();
|
||||||
|
const collectionField = getField(field.props.name);
|
||||||
|
const { modalProps } = useActionContext();
|
||||||
|
const handleSelect = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
insertSelector(schema.Selector);
|
||||||
|
setVisibleSelector(true);
|
||||||
|
setSelectedRows([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value && Object.keys(value).length > 0) {
|
||||||
|
setOptions(value);
|
||||||
|
} else {
|
||||||
|
setOptions(null);
|
||||||
|
}
|
||||||
|
}, [value, fieldNames?.label]);
|
||||||
|
|
||||||
|
const pickerProps = {
|
||||||
|
size: 'small',
|
||||||
|
fieldNames,
|
||||||
|
multiple: false,
|
||||||
|
association: {
|
||||||
|
target: collectionField?.target,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
onChange: props?.onChange,
|
||||||
|
selectedRows,
|
||||||
|
setSelectedRows,
|
||||||
|
collectionField,
|
||||||
|
};
|
||||||
|
const usePickActionProps = () => {
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
const { selectedRows, onChange } = useContext(RecordPickerContext);
|
||||||
|
return {
|
||||||
|
onClick() {
|
||||||
|
onChange(toValueItem(selectedRows?.[0]) || null);
|
||||||
|
setVisible(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const useTableSelectorProps = () => {
|
||||||
|
const {
|
||||||
|
multiple,
|
||||||
|
options,
|
||||||
|
setSelectedRows,
|
||||||
|
selectedRows: rcSelectRows = [],
|
||||||
|
onChange,
|
||||||
|
} = useContext(RecordPickerContext);
|
||||||
|
const { onRowSelectionChange, rowKey = 'id', ...others } = useTsp();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
return {
|
||||||
|
...others,
|
||||||
|
rowKey,
|
||||||
|
rowSelection: {
|
||||||
|
type: multiple ? 'checkbox' : 'radio',
|
||||||
|
selectedRowKeys: rcSelectRows?.filter((item) => options?.[rowKey] !== item[rowKey]).map((item) => item[rowKey]),
|
||||||
|
},
|
||||||
|
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
||||||
|
setSelectedRows?.(selectedRows);
|
||||||
|
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
||||||
|
onChange(toValueItem(selectedRows?.[0]) || null);
|
||||||
|
setVisible(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (underFilter) {
|
||||||
|
return <Input {...props} />;
|
||||||
|
}
|
||||||
|
console.log(collectionField);
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%', overflow: 'auto' }}>
|
||||||
|
<AssociationField.FileSelector
|
||||||
|
toValueItem={toValueItem}
|
||||||
|
value={options}
|
||||||
|
quickUpload={fieldSchema['x-component-props']?.quickUpload !== false}
|
||||||
|
selectFile={
|
||||||
|
collectionField?.target && collectionField?.target !== 'attachments'
|
||||||
|
? fieldSchema['x-component-props']?.selectFile !== false
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
action={`${collectionField?.target || 'attachments'}:create`}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<ActionContextProvider
|
||||||
|
value={{
|
||||||
|
openMode: 'drawer',
|
||||||
|
visible: visibleSelector,
|
||||||
|
setVisible: setVisibleSelector,
|
||||||
|
modalProps: {
|
||||||
|
getContainer: others?.getContainer || modalProps?.getContainer,
|
||||||
|
},
|
||||||
|
formValueChanged: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{collectionField?.target && collectionField?.target !== 'attachments' && (
|
||||||
|
<RecordPickerProvider {...pickerProps}>
|
||||||
|
<CollectionProvider_deprecated name={collectionField?.target} dataSource={'main'}>
|
||||||
|
<FormProvider>
|
||||||
|
<TableSelectorParamsProvider params={{}}>
|
||||||
|
<SchemaComponentOptions scope={{ usePickActionProps, useTableSelectorProps }}>
|
||||||
|
<RecursionField
|
||||||
|
onlyRenderProperties
|
||||||
|
basePath={field.address}
|
||||||
|
schema={fieldSchema}
|
||||||
|
filterProperties={(s) => {
|
||||||
|
return s['x-component'] === 'AssociationField.Selector';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
</TableSelectorParamsProvider>
|
||||||
|
</FormProvider>
|
||||||
|
</CollectionProvider_deprecated>
|
||||||
|
</RecordPickerProvider>
|
||||||
|
)}
|
||||||
|
</ActionContextProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 <EllipsisWithTooltip ellipsis>{value}</EllipsisWithTooltip>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<EllipsisWithTooltip ellipsis>{collectionField ? <Upload.ReadPretty {...props} /> : null}</EllipsisWithTooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AttachmentUrl = connect(InnerAttachmentUrl, mapReadPretty(FileManageReadPretty));
|
@ -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<any>(
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -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;
|
@ -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;
|
||||||
|
}
|
@ -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',
|
||||||
|
});
|
||||||
|
}
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -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<Field>();
|
||||||
|
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<Field>();
|
||||||
|
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<Field>();
|
||||||
|
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<Field>();
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -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';
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"Which file collection should it be uploaded to":"上传到文件表",
|
||||||
|
"Attachment (URL)":"附件 (URL)"
|
||||||
|
}
|
@ -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';
|
@ -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;
|
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
@ -0,0 +1 @@
|
|||||||
|
# @nocobase/plugin-workflow-response-message
|
2
packages/plugins/@nocobase/plugin-workflow-response-message/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-workflow-response-message/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/client';
|
||||||
|
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client/index.js');
|
@ -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"
|
||||||
|
}
|
2
packages/plugins/@nocobase/plugin-workflow-response-message/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-workflow-response-message/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/server';
|
||||||
|
export { default } from './dist/server';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/server/index.js');
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user