feat: init support departments (#788)

Co-authored-by: sealday <sealday@gmail.com>
Reviewed-on: daoyoucloud/tachybase#788
This commit is contained in:
sealday 2024-04-21 06:35:02 +08:00
parent 4bc1eb5ac2
commit c949bf4141
32 changed files with 4900 additions and 547 deletions

View File

@ -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": {

View File

@ -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

View File

@ -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');

View File

@ -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() {

View File

@ -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"
}

View File

@ -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": "年"
}

View File

@ -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();
};

View File

@ -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';

View 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();
};

View File

@ -0,0 +1,4 @@
import { defineCollection } from '@nocobase/database';
export default defineCollection({
name: 'departmentsRoles',
});

View File

@ -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,
],
});

View File

@ -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,
},
],
});

View File

@ -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',
},
],
});

View File

@ -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],
});

View File

@ -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,
});
}
}
}
}

View File

@ -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();
};

View File

@ -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';

View File

@ -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}`);
}
};

View File

@ -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();
}
};

View File

@ -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();
};

View File

@ -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,
},
});
}
}
};

View File

@ -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();
};

View File

@ -0,0 +1,12 @@
import { Model } from '@nocobase/database';
export class DepartmentModel extends Model {
getOwners() {
return this.getMembers({
through: {
where: {
isOwner: true,
},
},
});
}
}

View File

@ -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() {}

File diff suppressed because it is too large Load Diff