feat: init support departments (#788)
Co-authored-by: sealday <sealday@gmail.com> Reviewed-on: daoyoucloud/tachybase#788
This commit is contained in:
parent
4bc1eb5ac2
commit
c949bf4141
36
package.json
36
package.json
@ -41,37 +41,37 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"pm2": "^5.2.0",
|
||||
"rimraf": "^3.0.0"
|
||||
"pm2": "^5.3.1",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"@commitlint/cli": "^16.1.0",
|
||||
"@commitlint/config-conventional": "^16.0.0",
|
||||
"@commitlint/prompt-cli": "^16.1.0",
|
||||
"@commitlint/cli": "^16.3.0",
|
||||
"@commitlint/config-conventional": "^16.2.4",
|
||||
"@commitlint/prompt-cli": "^16.3.0",
|
||||
"@nocobase/build": "workspace:*",
|
||||
"@nocobase/test": "workspace:*",
|
||||
"@tachybase/cli": "workspace:*",
|
||||
"@tachybase/preset-tachybase": "workspace:*",
|
||||
"@types/react": "^18.2.73",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"auto-changelog": "^2.4.0",
|
||||
"axios": "^1.6.2",
|
||||
"commander": "^9.2.0",
|
||||
"eslint-plugin-jest-dom": "^5.0.1",
|
||||
"eslint-plugin-testing-library": "^5.11.0",
|
||||
"axios": "^1.6.8",
|
||||
"commander": "^9.5.0",
|
||||
"eslint-plugin-jest-dom": "^5.4.0",
|
||||
"eslint-plugin-testing-library": "^5.11.1",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^13.2.3",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-packagejson": "^2.4.14",
|
||||
"prettier-plugin-sql": "^0.17.0",
|
||||
"pretty-format": "^24.0.0",
|
||||
"pretty-quick": "^3.1.0",
|
||||
"lint-staged": "^13.3.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-packagejson": "^2.5.0",
|
||||
"prettier-plugin-sql": "^0.17.1",
|
||||
"pretty-format": "^24.9.0",
|
||||
"pretty-quick": "^3.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "5.4.5",
|
||||
"umi": "^4.1.8"
|
||||
"umi": "^4.1.10"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.5",
|
||||
"engines": {
|
||||
|
@ -42,6 +42,7 @@
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nocobase/plugin-acl": "workspace:*",
|
||||
"@nocobase/actions": "workspace:*",
|
||||
"@nocobase/client": "workspace:*",
|
||||
"@nocobase/database": "workspace:*",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
import { useFieldSchema } from '@nocobase/schema';
|
||||
import { App } from 'antd';
|
||||
import { useMatch } from 'react-router-dom';
|
||||
import { useTranslation } from '../locale';
|
||||
import { useTranslation } from '../../locale';
|
||||
|
||||
export function usePageSettingsProps() {
|
||||
const isAdmin = useMatch('/admin/:name');
|
@ -74,7 +74,8 @@ import {
|
||||
useCustomPresets1,
|
||||
} from './schema-settings/SchemaSettingsDatePresets';
|
||||
import { SchemaSettingsSubmitDataType } from './schema-settings/SchemaSettingsSubmitDataType';
|
||||
import { EmbedPlugin } from './embed';
|
||||
import { EmbedPlugin } from './features/embed';
|
||||
import { DepartmentsPlugin } from './features/departments';
|
||||
export { usePDFViewerRef } from './schema-initializer';
|
||||
export * from './components/custom-components/custom-components';
|
||||
|
||||
@ -84,6 +85,7 @@ export class PluginCoreClient extends Plugin {
|
||||
async afterAdd() {
|
||||
await this.app.pm.add(GroupBlockPlugin);
|
||||
await this.app.pm.add(EmbedPlugin);
|
||||
await this.app.pm.add(DepartmentsPlugin);
|
||||
}
|
||||
|
||||
async registerSettings() {
|
||||
|
@ -1,23 +1,57 @@
|
||||
{
|
||||
"Add custom component": "Add custom component",
|
||||
"Add department": "Add department",
|
||||
"Add departments": "Add departments",
|
||||
"Add members": "Add members",
|
||||
"All users": "All users",
|
||||
"Are you sure you want to remove it?": "Are you sure you want to remove it?",
|
||||
"Are you sure you want to remove these departments?": "Are you sure you want to remove these departments?",
|
||||
"Are you sure you want to remove these members?": "Are you sure you want to remove these members?",
|
||||
"Association field": "Association field",
|
||||
"Chuangxing presets": "Chuangxing presets",
|
||||
"Configure": "Configure",
|
||||
"Custom filter": "Custom filter",
|
||||
"Custom option label": "Custom option label",
|
||||
"Delete department": "Delete department",
|
||||
"Department name": "Department name",
|
||||
"Department": "Department",
|
||||
"Departments management": "Departments management",
|
||||
"Departments": "Departments",
|
||||
"Edit department": "Edit department",
|
||||
"Group block": "Group block",
|
||||
"Group": "Group",
|
||||
"HomePage Config": "HomePage Config",
|
||||
"Last month": "Last month",
|
||||
"Main department": "Main department",
|
||||
"Main": "Main",
|
||||
"Mobile UI": "Mobile UI",
|
||||
"Navigate to new page": "Navigate to new page",
|
||||
"New department": "New department",
|
||||
"New sub department": "New sub department",
|
||||
"Owner": "Owner",
|
||||
"Owners": "Owners",
|
||||
"Page style": "Page Style",
|
||||
"Pagination": "Pagination",
|
||||
"Pick a data entry for viewing and editing": "Pick a data entry for viewing and editing",
|
||||
"Please select departments": "Please select departments",
|
||||
"Please select members": "Please select members",
|
||||
"Please select": "Please select",
|
||||
"Remove department": "Remove department",
|
||||
"Remove departments": "Remove departments",
|
||||
"Remove member": "Remove member",
|
||||
"Remove members": "Remove members",
|
||||
"Roles management": "Roles management",
|
||||
"Search for departments, users": "Search for departments, users",
|
||||
"Search results": "Search results",
|
||||
"Select submit data type": "Select submit data type",
|
||||
"Set as main department": "Set as main department",
|
||||
"Signature input": "Signature input",
|
||||
"Something went wrong. Please contact the developer to resolve the issue.": "Something went wrong. Please contact the developer to resolve the issue.",
|
||||
"Superior department": "Superior department",
|
||||
"System setting": "System setting",
|
||||
"The department has members, please remove them first": "The department has members, please remove them first",
|
||||
"The department has sub-departments, please delete them first": "The department has sub-departments, please delete them first",
|
||||
"This field is currently not supported for use in form blocks.": "This field is currently not supported for use in form blocks.",
|
||||
"This month": "This month",
|
||||
"This year": "This year",
|
||||
"classical": "classical",
|
||||
@ -27,6 +61,5 @@
|
||||
"loading...": "loading...",
|
||||
"preview block": "preview block",
|
||||
"tabs": "tabs",
|
||||
"year": "year",
|
||||
"Select submit data type": "Select submit data type"
|
||||
"year": "year"
|
||||
}
|
||||
|
@ -1,23 +1,57 @@
|
||||
{
|
||||
"Add custom component": "添加自定义组件",
|
||||
"Add department": "添加部门",
|
||||
"Add departments": "添加部门",
|
||||
"Add members": "添加成员",
|
||||
"All users": "所有用户",
|
||||
"Are you sure you want to remove it?": "你确定要移除吗?",
|
||||
"Are you sure you want to remove these departments?": "你确定要移除这些部门吗?",
|
||||
"Are you sure you want to remove these members?": "你确定要移除这些成员吗?",
|
||||
"Association field": "关联字段",
|
||||
"Chuangxing presets": "创兴预置",
|
||||
"Configure": "配置",
|
||||
"Custom filter": "自定义筛选",
|
||||
"Custom option label": "自定义选项标签",
|
||||
"Group": "汇总",
|
||||
"Delete department": "删除部门",
|
||||
"Department name": "部门名称",
|
||||
"Department": "部门",
|
||||
"Departments management": "部门管理",
|
||||
"Departments": "部门",
|
||||
"Edit department": "编辑部门",
|
||||
"Group block": "汇总区块",
|
||||
"Group": "汇总",
|
||||
"HomePage Config": "主页配置",
|
||||
"Last month": "上月",
|
||||
"Main department": "主属部门",
|
||||
"Main": "主属部门",
|
||||
"Mobile UI": "移动端页面",
|
||||
"Navigate to new page": "导航到新页面",
|
||||
"New department": "新建部门",
|
||||
"New sub department": "新建子部门",
|
||||
"Owner": "负责人",
|
||||
"Owners": "负责人",
|
||||
"Page style": "页面版本",
|
||||
"Pagination": "分页",
|
||||
"Pick a data entry for viewing and editing": "选择一条数据用于查看与编辑",
|
||||
"Please select departments": "请选择部门",
|
||||
"Please select members": "请选择成员",
|
||||
"Please select": "请选择",
|
||||
"Remove department": "移除部门",
|
||||
"Remove departments": "移除部门",
|
||||
"Remove member": "移除成员",
|
||||
"Remove members": "移除成员",
|
||||
"Roles management": "角色管理",
|
||||
"Search for departments, users": "搜索部门、用户",
|
||||
"Search results": "搜索结果",
|
||||
"Select submit data type": "增量提交",
|
||||
"Set as main department": "设置为主属部门",
|
||||
"Signature input": "手写签名",
|
||||
"Something went wrong. Please contact the developer to resolve the issue.": "系统内部错误,请联系开发者解决",
|
||||
"Superior department": "上级部门",
|
||||
"System setting": "系统设置",
|
||||
"The department has members, please remove them first": "部门下有成员,请先移除",
|
||||
"The department has sub-departments, please delete them first": "部门下有子部门,请先删除子部门",
|
||||
"This field is currently not supported for use in form blocks.": "该字段目前不支持在表单区块中使用。",
|
||||
"This month": "本月",
|
||||
"This year": "今年",
|
||||
"classical": "经典版",
|
||||
@ -27,6 +61,5 @@
|
||||
"loading...": "加载中...",
|
||||
"preview block": "预览区块",
|
||||
"tabs": "多标签",
|
||||
"year": "年",
|
||||
"Select submit data type": "增量提交"
|
||||
}
|
||||
"year": "年"
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
import type { Context, Next } from '@nocobase/actions';
|
||||
|
||||
export const getAppendsOwners = async (ctx: Context, next: Next) => {
|
||||
const { filterByTk, appends } = ctx.action.params;
|
||||
const repo = ctx.db.getRepository('departments');
|
||||
const department = 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();
|
||||
};
|
@ -3,3 +3,5 @@ export * from './token-configuration-controller';
|
||||
export * from './plugin-version';
|
||||
export * from './link-manager-controller';
|
||||
export * from './system-message-controller';
|
||||
export * from './departments';
|
||||
export * from './users';
|
||||
|
112
packages/plugins/@hera/plugin-core/src/server/actions/users.ts
Normal file
112
packages/plugins/@hera/plugin-core/src/server/actions/users.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { DEFAULT_PAGE, DEFAULT_PER_PAGE, type Context, type 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) => 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) => department.id);
|
||||
const main = departments.find((department) => department.isMain);
|
||||
const owners = departments.filter((department) => 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) => 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,4 @@
|
||||
import { defineCollection } from '@nocobase/database';
|
||||
export default defineCollection({
|
||||
name: 'departmentsRoles',
|
||||
});
|
@ -0,0 +1,134 @@
|
||||
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',
|
||||
title: '{{t("Departments")}}',
|
||||
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,20 @@
|
||||
import { defineCollection } from '@nocobase/database';
|
||||
export default defineCollection({
|
||||
name: 'departmentsUsers',
|
||||
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,17 @@
|
||||
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,58 @@
|
||||
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,117 @@
|
||||
import type { CollectionRepository } from '@nocobase/plugin-collection-manager';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { resolve } from 'path';
|
||||
import {
|
||||
aggregateSearch,
|
||||
listExcludeDept,
|
||||
removeOwner,
|
||||
setMainDepartment as setMainDepartmentAction,
|
||||
setOwner,
|
||||
} from '../actions';
|
||||
import { departmentsField, mainDepartmentField } from '../collections/users';
|
||||
import {
|
||||
destroyDepartmentCheck,
|
||||
resetUserDepartmentsCache,
|
||||
setDepartmentOwners,
|
||||
setDepartmentsInfo,
|
||||
setMainDepartment,
|
||||
updateDepartmentIsLeaf,
|
||||
} from '../middlewares';
|
||||
import { DepartmentModel } from '../models/department';
|
||||
|
||||
export class DepartmentsPlugin extends Plugin {
|
||||
beforeLoad() {
|
||||
this.app.db.registerModels({ DepartmentModel });
|
||||
this.app.acl.addFixedParams('collections', 'destroy', () => {
|
||||
return {
|
||||
filter: {
|
||||
'name.$notIn': ['departments', 'departmentsUsers', 'departmentsRoles'],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
async load() {
|
||||
this.app.resource({
|
||||
name: 'users',
|
||||
actions: {
|
||||
listExcludeDept: listExcludeDept,
|
||||
setMainDepartment: setMainDepartmentAction,
|
||||
},
|
||||
});
|
||||
this.app.resource({
|
||||
name: 'departments',
|
||||
actions: {
|
||||
aggregateSearch: aggregateSearch,
|
||||
setOwner: setOwner,
|
||||
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',
|
||||
'roles.departments:add',
|
||||
'roles.departments:remove',
|
||||
'users.departments:add',
|
||||
'users.departments:remove',
|
||||
],
|
||||
});
|
||||
this.app.resourcer.use(setDepartmentsInfo, {
|
||||
tag: 'setDepartmentsInfo',
|
||||
before: 'setCurrentRole',
|
||||
after: 'auth',
|
||||
});
|
||||
this.app.resourcer.use(setDepartmentOwners);
|
||||
this.app.resourcer.use(destroyDepartmentCheck);
|
||||
this.app.resourcer.use(updateDepartmentIsLeaf);
|
||||
this.app.resourcer.use(resetUserDepartmentsCache);
|
||||
this.app.resourcer.use(setMainDepartment);
|
||||
this.app.db.on('departmentsUsers.afterSave', async (model) => {
|
||||
const cache = this.app.cache;
|
||||
await cache.del(`departments:${model.get('userId')}`);
|
||||
});
|
||||
this.app.db.on('departmentsUsers.afterDestroy', async (model) => {
|
||||
const cache = this.app.cache;
|
||||
await cache.del(`departments:${model.get('userId')}`);
|
||||
});
|
||||
this.app.on('beforeSignOut', ({ userId }) => {
|
||||
this.app.cache.del(`departments:${userId}`);
|
||||
});
|
||||
}
|
||||
async install(options) {
|
||||
const collectionRepo = this.db.getRepository<CollectionRepository>('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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import type { 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);
|
||||
}
|
||||
await next();
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
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,20 @@
|
||||
import type { Context, Next } from '@nocobase/actions';
|
||||
|
||||
export const resetUserDepartmentsCache = async (ctx: Context, next: Next) => {
|
||||
await next();
|
||||
const { associatedName, resourceName, associatedIndex, actionName, values } = ctx.action.params;
|
||||
const cache = ctx.app.cache;
|
||||
if (
|
||||
associatedName === 'departments' &&
|
||||
resourceName === 'members' &&
|
||||
['add', 'remove', 'set'].includes(actionName) &&
|
||||
(values == null ? void 0 : values.length)
|
||||
) {
|
||||
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,40 @@
|
||||
import type { Context, Next } from '@nocobase/actions';
|
||||
import _ from 'lodash';
|
||||
|
||||
const setOwners = async (ctx: Context, filterByTk, owners) => {
|
||||
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) => 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 = _.omit(values, ['owners']);
|
||||
await next();
|
||||
await setOwners(ctx, filterByTk, owners);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import type { Context, Next } from '@nocobase/actions';
|
||||
import type { ArrayFieldRepository } from '@nocobase/database';
|
||||
|
||||
export const setDepartmentsInfo = async (ctx: Context, next: Next) => {
|
||||
const currentUser = ctx.state.currentUser;
|
||||
if (!currentUser) {
|
||||
return next();
|
||||
}
|
||||
const cache = ctx.cache;
|
||||
const repo = ctx.db.getRepository<ArrayFieldRepository>('users.departments', currentUser.id);
|
||||
const departments = await cache.wrap(`departments:${currentUser.id}`, () =>
|
||||
repo.find({
|
||||
appends: ['owners', 'roles', 'parent(recursively=true)'],
|
||||
raw: true,
|
||||
}),
|
||||
);
|
||||
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) => rolesMap.set(role.name, role));
|
||||
ctx.state.attachRoles = Array.from(rolesMap.values());
|
||||
await next();
|
||||
};
|
@ -0,0 +1,81 @@
|
||||
import type { 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 == null ? void 0 : 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,60 @@
|
||||
import type { Context, Next } from '@nocobase/actions';
|
||||
|
||||
const updateIsLeafWhenAddChild = async (repo, parent) => {
|
||||
if (parent && parent.isLeaf !== false) {
|
||||
await repo.update({
|
||||
filter: {
|
||||
id: parent.id,
|
||||
},
|
||||
values: {
|
||||
isLeaf: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const updateIsLeafWhenChangeChild = async (repo, oldParentId, newParentId) => {
|
||||
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 == null ? void 0 : 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,12 @@
|
||||
import { Model } from '@nocobase/database';
|
||||
export class DepartmentModel extends Model {
|
||||
getOwners() {
|
||||
return this.getMembers({
|
||||
through: {
|
||||
where: {
|
||||
isOwner: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@ -1,23 +1,31 @@
|
||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
import Application, { InstallOptions, Plugin, type PluginOptions } from '@nocobase/server';
|
||||
import path from 'path';
|
||||
import CalcField from './fields/calc';
|
||||
import { SqlLoader } from './services/sql-loader';
|
||||
import { ConnectionManager } from './services/connection-manager';
|
||||
import { isMain } from './utils/multiprocess';
|
||||
import { FontManager } from './services/font-manager';
|
||||
import { HomePageService } from './services/home-page-service';
|
||||
import { WebControllerService as WebService } from './services/web-service';
|
||||
import './actions';
|
||||
import { Container } from '@nocobase/utils';
|
||||
import { DepartmentsPlugin } from './features/departments';
|
||||
|
||||
export class PluginCoreServer extends Plugin {
|
||||
afterAdd() {
|
||||
pluginDepartments: DepartmentsPlugin;
|
||||
constructor(app: Application, options?: PluginOptions) {
|
||||
super(app, options);
|
||||
this.pluginDepartments = new DepartmentsPlugin(app, options);
|
||||
}
|
||||
async afterAdd() {
|
||||
this.db.registerFieldTypes({
|
||||
calc: CalcField,
|
||||
});
|
||||
}
|
||||
beforeLoad() {}
|
||||
beforeLoad() {
|
||||
this.pluginDepartments.beforeLoad();
|
||||
}
|
||||
async load() {
|
||||
await this.pluginDepartments.load();
|
||||
Container.reset();
|
||||
try {
|
||||
Container.set({ id: 'db', value: this.db });
|
||||
@ -35,7 +43,9 @@ export class PluginCoreServer extends Plugin {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
async install(options?: InstallOptions) {}
|
||||
async install(options?: InstallOptions) {
|
||||
this.pluginDepartments.install(options);
|
||||
}
|
||||
async afterEnable() {}
|
||||
async afterDisable() {}
|
||||
async remove() {}
|
||||
|
2002
pnpm-lock.yaml
2002
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user