mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 23:49:27 +08:00
Merge branch 'develop' of github.com:nocobase/nocobase into feat-main-datasource-mssql
This commit is contained in:
commit
1e4de52282
33
CHANGELOG.md
33
CHANGELOG.md
@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- Fix the issue of preview images being obscured ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- In the form block, the default value of the field configuration will first be displayed as the original variable string and then disappear ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe
|
||||||
|
|
||||||
|
## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11
|
||||||
|
|
||||||
|
### 🚀 Improvements
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- Add default type fallback API for `Variable.Input` ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher
|
||||||
|
|
||||||
|
- Optimize prompts for unconfigured pages ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- **[Workflow: Delay node]** Support to use variable for duration ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher
|
||||||
|
|
||||||
|
- **[Workflow: Custom action event]** Add refresh settings for trigger workflow button by @mytharcher
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- subtable description overlapping with add new button ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh
|
||||||
|
|
||||||
|
- dashed underline caused by horizontal form layout in modal ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[File storage: S3(Pro)]** Fix missing await for next call. by @jiannx
|
||||||
|
|
||||||
|
- **[Email manager]** Fix missing await for next call. by @jiannx
|
||||||
|
|
||||||
## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
|
## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
|
||||||
|
|
||||||
### 🚀 Improvements
|
### 🚀 Improvements
|
||||||
|
@ -5,6 +5,39 @@
|
|||||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||||
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
||||||
|
|
||||||
|
## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 修复预览图片被遮挡的问题 ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- 表单区块中,字段配置的默认值会先显示为原始变量字符串然后再消失 ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe
|
||||||
|
|
||||||
|
## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11
|
||||||
|
|
||||||
|
### 🚀 优化
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 为 `Variable.Input` 组件增加默认退避类型的 API ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher
|
||||||
|
|
||||||
|
- 优化未配置页面时的提示 ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- **[工作流:延时节点]** 支持延迟时间使用变量 ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher
|
||||||
|
|
||||||
|
- **[工作流:自定义操作事件]** 为触发工作流按钮增加刷新配置项 by @mytharcher
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 子表格中描述信息与操作按钮遮挡 ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh
|
||||||
|
|
||||||
|
- 弹窗表单在 horizontal 布局下初始宽度计算错误,导致出现提示和 下划虚线 ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[文件存储:S3 (Pro)]** 修复next调用缺少await by @jiannx
|
||||||
|
|
||||||
|
- **[邮件管理]** 修复next调用缺少await by @jiannx
|
||||||
|
|
||||||
## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
|
## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
|
||||||
|
|
||||||
### 🚀 优化
|
### 🚀 优化
|
||||||
|
@ -461,7 +461,7 @@ exports.initEnv = function initEnv() {
|
|||||||
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 pkgDir = resolve(process.cwd(), 'storage/plugins', '@nocobase/plugin-multi-app-manager');
|
||||||
fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { force: true });
|
fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.generatePlugins = function () {
|
exports.generatePlugins = function () {
|
||||||
|
@ -10,11 +10,11 @@
|
|||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
|
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
|
||||||
import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component';
|
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
|
||||||
|
import { ActionBarProvider, DatePickerProvider, SchemaComponentOptions } from '../schema-component';
|
||||||
import { DefaultValueProvider } from '../schema-settings';
|
import { DefaultValueProvider } from '../schema-settings';
|
||||||
import { CollectOperators } from './CollectOperators';
|
import { CollectOperators } from './CollectOperators';
|
||||||
import { FormBlockProvider } from './FormBlockProvider';
|
import { FormBlockProvider } from './FormBlockProvider';
|
||||||
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
|
|
||||||
|
|
||||||
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
||||||
const filedSchema = useFieldSchema();
|
const filedSchema = useFieldSchema();
|
||||||
@ -35,7 +35,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
|
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
|
||||||
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
|
<FormBlockProvider name="filter-form" {...props} confirmBeforeClose={false}></FormBlockProvider>
|
||||||
</DefaultValueProvider>
|
</DefaultValueProvider>
|
||||||
</ActionBarProvider>
|
</ActionBarProvider>
|
||||||
</DatePickerProvider>
|
</DatePickerProvider>
|
||||||
|
@ -546,9 +546,11 @@ export const useFilterBlockActionProps = () => {
|
|||||||
const { doFilter } = useDoFilter();
|
const { doFilter } = useDoFilter();
|
||||||
const actionField = useField();
|
const actionField = useField();
|
||||||
actionField.data = actionField.data || {};
|
actionField.data = actionField.data || {};
|
||||||
|
const form = useForm();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async onClick() {
|
async onClick() {
|
||||||
|
await form.submit();
|
||||||
actionField.data.loading = true;
|
actionField.data.loading = true;
|
||||||
await doFilter();
|
await doFilter();
|
||||||
actionField.data.loading = false;
|
actionField.data.loading = false;
|
||||||
|
@ -18,6 +18,7 @@ import { useCollectionFieldUISchema, useIsInNocoBaseRecursionFieldContext } from
|
|||||||
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
|
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
|
||||||
import { useCompile, useComponent } from '../../schema-component';
|
import { useCompile, useComponent } from '../../schema-component';
|
||||||
import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||||
|
import { isVariable } from '../../variables/utils/isVariable';
|
||||||
import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider';
|
import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -135,7 +136,14 @@ const CollectionFieldInternalField = (props) => {
|
|||||||
|
|
||||||
if (!uiSchema) return null;
|
if (!uiSchema) return null;
|
||||||
|
|
||||||
return <Component {...props} {...dynamicProps} />;
|
const mergedProps = { ...props, ...dynamicProps };
|
||||||
|
|
||||||
|
// Prevent displaying the variable string first, then the variable value
|
||||||
|
if (isVariable(mergedProps.value) && mergedProps.value === fieldSchema.default) {
|
||||||
|
mergedProps.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component {...mergedProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CollectionField = connect((props) => {
|
export const CollectionField = connect((props) => {
|
||||||
|
@ -1085,5 +1085,8 @@
|
|||||||
"Select data blocks to refresh": "Aggiorna blocchi di dati",
|
"Select data blocks to refresh": "Aggiorna blocchi di dati",
|
||||||
"After successful submission, the selected data blocks will be automatically refreshed.": "Dopo una soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés.",
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Dopo una soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés.",
|
||||||
"No pages yet, please configure first": "Nessuna pagina ancora, si prega di configurare prima",
|
"No pages yet, please configure first": "Nessuna pagina ancora, si prega di configurare prima",
|
||||||
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur"
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
|
||||||
|
"Refresh data blocks": "Aggiorna blocchi di dati",
|
||||||
|
"Select data blocks to refresh": "Aggiorna blocchi di dati",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Dopo una soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés."
|
||||||
}
|
}
|
||||||
|
@ -787,5 +787,8 @@
|
|||||||
"Select data blocks to refresh": "Selecionar blocos de dados para atualizar",
|
"Select data blocks to refresh": "Selecionar blocos de dados para atualizar",
|
||||||
"After successful submission, the selected data blocks will be automatically refreshed.": "Após a atualização em massa bem sucedida.",
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Após a atualização em massa bem sucedida.",
|
||||||
"No pages yet, please configure first": "Ainda não há páginas, por favor configure primeiro",
|
"No pages yet, please configure first": "Ainda não há páginas, por favor configure primeiro",
|
||||||
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur"
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
|
||||||
|
"Refresh data blocks": "Atualizar blocos de dados",
|
||||||
|
"Select data blocks to refresh": "Selecionar blocos de dados para atualizar",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Após a atualização em massa bem sucedida."
|
||||||
}
|
}
|
||||||
|
@ -616,5 +616,8 @@
|
|||||||
"Select data blocks to refresh": "Выберите блоки данных для обновления",
|
"Select data blocks to refresh": "Выберите блоки данных для обновления",
|
||||||
"After successful submission, the selected data blocks will be automatically refreshed.": "После успешной отправки выбранные блоки данных будут автоматически обновлены.",
|
"After successful submission, the selected data blocks will be automatically refreshed.": "После успешной отправки выбранные блоки данных будут автоматически обновлены.",
|
||||||
"No pages yet, please configure first": "Пока нет страниц, пожалуйста, настройте сначала",
|
"No pages yet, please configure first": "Пока нет страниц, пожалуйста, настройте сначала",
|
||||||
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Нажмите на значок \"Редактор пользовательского интерфейса\" в правом верхнем углу, чтобы войти в режим редактора пользовательского интерфейса"
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Нажмите на значок \"Редактор пользовательского интерфейса\" в правом верхнем углу, чтобы войти в режим редактора пользовательского интерфейса",
|
||||||
|
"Refresh data blocks": "Обновить блоки данных",
|
||||||
|
"Select data blocks to refresh": "Выберите блоки данных для обновления",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "После успешной отправки выбранные блоки данных будут автоматически обновлены."
|
||||||
}
|
}
|
||||||
|
@ -614,5 +614,8 @@
|
|||||||
"Select data blocks to refresh": "Veri bloklarını yenilemek için seçin",
|
"Select data blocks to refresh": "Veri bloklarını yenilemek için seçin",
|
||||||
"After successful submission, the selected data blocks will be automatically refreshed.": "Başarılı bir şekilde gönderildikten sonra, seçilen veri blokları otomatik olarak yenilenecektir.",
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Başarılı bir şekilde gönderildikten sonra, seçilen veri blokları otomatik olarak yenilenecektir.",
|
||||||
"No pages yet, please configure first": "Henüz sayfa yok, lütfen önce yapılandırın",
|
"No pages yet, please configure first": "Henüz sayfa yok, lütfen önce yapılandırın",
|
||||||
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Kullanıcı arayüzü düzenleyici moduna girmek için sağ üst köşedeki \"Kullanıcı Arayüzü Düzenleyici\" simgesine tıklayın"
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Kullanıcı arayüzü düzenleyici moduna girmek için sağ üst köşedeki \"Kullanıcı Arayüzü Düzenleyici\" simgesine tıklayın",
|
||||||
|
"Refresh data blocks": "Yenile veri blokları",
|
||||||
|
"Select data blocks to refresh": "Veri bloklarını yenilemek için seçin",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Başarılı bir şekilde gönderildikten sonra, seçilen veri blokları otomatik olarak yenilenecektir."
|
||||||
}
|
}
|
||||||
|
@ -830,5 +830,8 @@
|
|||||||
"Select data blocks to refresh": "Виберіть блоки даних для оновлення",
|
"Select data blocks to refresh": "Виберіть блоки даних для оновлення",
|
||||||
"After successful submission, the selected data blocks will be automatically refreshed.": "Після успішної подачі вибрані блоки даних будуть автоматично оновлені.",
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Після успішної подачі вибрані блоки даних будуть автоматично оновлені.",
|
||||||
"No pages yet, please configure first": "Ще немає сторінок, будь ласка, спочатку налаштуйте",
|
"No pages yet, please configure first": "Ще немає сторінок, будь ласка, спочатку налаштуйте",
|
||||||
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Натисніть на значок \"Редактор користувацького інтерфейсу\" в правому верхньому куті, щоб увійти в режим редактора користувацького інтерфейсу."
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Натисніть на значок \"Редактор користувацького інтерфейсу\" в правому верхньому куті, щоб увійти в режим редактора користувацького інтерфейсу.",
|
||||||
|
"Refresh data blocks": "Оновити дані блоків",
|
||||||
|
"Select data blocks to refresh": "Виберіть блоки даних для оновлення",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Після успішної подачі вибрані блоки даних будуть автоматично оновлені."
|
||||||
}
|
}
|
||||||
|
@ -1102,5 +1102,6 @@
|
|||||||
"Colon":"冒号",
|
"Colon":"冒号",
|
||||||
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。",
|
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。",
|
||||||
"No pages yet, please configure first": "暂无页面,请先配置",
|
"No pages yet, please configure first": "暂无页面,请先配置",
|
||||||
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "点击右上角的“界面配置”图标,进入界面配置模式"
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "点击右上角的“界面配置”图标,进入界面配置模式",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。"
|
||||||
}
|
}
|
||||||
|
@ -921,5 +921,8 @@
|
|||||||
"Select data blocks to refresh": "選擇要刷新的數據區塊",
|
"Select data blocks to refresh": "選擇要刷新的數據區塊",
|
||||||
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功後,選中的數據區塊將自動刷新。",
|
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功後,選中的數據區塊將自動刷新。",
|
||||||
"No pages yet, please configure first": "尚未配置頁面,請先配置",
|
"No pages yet, please configure first": "尚未配置頁面,請先配置",
|
||||||
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "點擊右上角的 \"介面設定\" 圖示進入介面設定模式"
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "點擊右上角的 \"介面設定\" 圖示進入介面設定模式",
|
||||||
|
"Refresh data blocks": "刷新數據區塊",
|
||||||
|
"Select data blocks to refresh": "選擇要刷新的數據區塊",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功後,選中的數據區塊將自動刷新。"
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,11 @@ export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
field.dataSource = uiSchema.enum;
|
field.dataSource = uiSchema.enum;
|
||||||
const originalProps =
|
const originalProps =
|
||||||
compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {};
|
compile({
|
||||||
|
...(operator?.schema?.['x-component-props'] || {}),
|
||||||
|
...(uiSchema['x-component-props'] || {}),
|
||||||
|
...(fieldSchema?.['x-component-props'] || {}),
|
||||||
|
}) || {};
|
||||||
|
|
||||||
field.componentProps = merge(field.componentProps || {}, originalProps, dynamicProps || {});
|
field.componentProps = merge(field.componentProps || {}, originalProps, dynamicProps || {});
|
||||||
}, [uiSchemaOrigin]);
|
}, [uiSchemaOrigin]);
|
||||||
|
@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* 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 { findFirstPageRoute, NocoBaseDesktopRouteType } from '..';
|
||||||
|
import { NocoBaseDesktopRoute } from '../convertRoutesToSchema';
|
||||||
|
|
||||||
|
describe('findFirstPageRoute', () => {
|
||||||
|
// 基本测试:空路由数组
|
||||||
|
it('should return undefined for empty routes array', () => {
|
||||||
|
const result = findFirstPageRoute([]);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 基本测试:undefined 路由数组
|
||||||
|
it('should return undefined for undefined routes', () => {
|
||||||
|
const result = findFirstPageRoute(undefined);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:只有一个页面路由
|
||||||
|
it('should find the first page route when there is only one page', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:多个页面路由
|
||||||
|
it('should find the first page route when there are multiple pages', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
schemaUid: 'page2',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:不同类型的路由混合
|
||||||
|
it('should find the first page route among mixed route types', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
schemaUid: 'link1',
|
||||||
|
type: NocoBaseDesktopRouteType.link,
|
||||||
|
title: 'Link 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:隐藏的菜单项
|
||||||
|
it('should ignore hidden menu items', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
hideInMenu: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
schemaUid: 'page2',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:嵌套路由
|
||||||
|
it('should find page route in nested group', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[0].children[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:多层嵌套路由
|
||||||
|
it('should find page route in deeply nested groups', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 111,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[0].children[0].children[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:复杂路由结构
|
||||||
|
it('should find the first visible page in a complex route structure', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 1',
|
||||||
|
hideInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
schemaUid: 'page2',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[1].children[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:空组
|
||||||
|
it('should skip empty groups and find page in next group', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Empty Group',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[1]);
|
||||||
|
});
|
||||||
|
});
|
@ -723,27 +723,11 @@ export const InternalAdminLayout = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDefaultPageUid(routes: NocoBaseDesktopRoute[]) {
|
|
||||||
// Find the first route of type "page"
|
|
||||||
for (const route of routes) {
|
|
||||||
if (route.type === NocoBaseDesktopRouteType.page) {
|
|
||||||
return route.schemaUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.children?.length) {
|
|
||||||
const result = getDefaultPageUid(route.children);
|
|
||||||
if (result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const NavigateToDefaultPage: FC = (props) => {
|
const NavigateToDefaultPage: FC = (props) => {
|
||||||
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
||||||
const location = useLocationNoUpdate();
|
const location = useLocationNoUpdate();
|
||||||
|
|
||||||
const defaultPageUid = getDefaultPageUid(allAccessRoutes);
|
const defaultPageUid = findFirstPageRoute(allAccessRoutes)?.schemaUid;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -975,16 +959,17 @@ function findRouteById(id: string, treeArray: any[]) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
|
export function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
|
||||||
if (!routes) return;
|
if (!routes) return;
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes.filter((item) => !item.hideInMenu)) {
|
||||||
if (route.type === NocoBaseDesktopRouteType.page) {
|
if (route.type === NocoBaseDesktopRouteType.page) {
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.children?.length) {
|
if (route.type === NocoBaseDesktopRouteType.group && route.children?.length) {
|
||||||
return findFirstPageRoute(route.children);
|
const result = findFirstPageRoute(route.children);
|
||||||
|
if (result) return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,6 @@ export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
|
|||||||
},
|
},
|
||||||
[footerNodeName],
|
[footerNodeName],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionContextNoRerender>
|
<ActionContextNoRerender>
|
||||||
<zIndexContext.Provider value={zIndex}>
|
<zIndexContext.Provider value={zIndex}>
|
||||||
@ -129,7 +128,7 @@ export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
|
|||||||
<Drawer
|
<Drawer
|
||||||
zIndex={zIndex}
|
zIndex={zIndex}
|
||||||
width={openSizeWidthMap.get(openSize)}
|
width={openSizeWidthMap.get(openSize)}
|
||||||
title={t(field.title, { ns: NAMESPACE_UI_SCHEMA })}
|
title={typeof field.title === 'string' ? t(field.title, { ns: NAMESPACE_UI_SCHEMA }) : field.title}
|
||||||
{...others}
|
{...others}
|
||||||
{...drawerProps}
|
{...drawerProps}
|
||||||
rootStyle={rootStyle}
|
rootStyle={rootStyle}
|
||||||
|
@ -72,7 +72,7 @@ const InternalActionBar: FC = (props: any) => {
|
|||||||
<Portal>
|
<Portal>
|
||||||
<DndContext>
|
<DndContext>
|
||||||
<div
|
<div
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 8, ...style, marginTop: 0 }}
|
style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 0, ...style }}
|
||||||
{...others}
|
{...others}
|
||||||
className={cx(others.className, 'nb-action-bar')}
|
className={cx(others.className, 'nb-action-bar')}
|
||||||
>
|
>
|
||||||
|
@ -39,15 +39,17 @@ const formItemWrapCss = css`
|
|||||||
.ant-description-textarea img {
|
.ant-description-textarea img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
&.ant-formily-item-layout-vertical .ant-formily-item-label {
|
&.ant-formily-item-layout-horizontal.ant-formily-item-label-wrap {
|
||||||
|
.ant-formily-item-label {
|
||||||
display: inline;
|
display: inline;
|
||||||
.ant-formily-item-label-tooltip-icon {
|
padding-right: 5px;
|
||||||
display: inline;
|
|
||||||
}
|
.ant-formily-item-label-tooltip-icon,
|
||||||
.ant-formily-item-label-content {
|
.ant-formily-item-label-content {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const formItemLabelCss = css`
|
const formItemLabelCss = css`
|
||||||
|
@ -86,11 +86,6 @@ const useParseDefaultValue = () => {
|
|||||||
field &&
|
field &&
|
||||||
((isVariable(fieldSchema.default) && field.value == null) || field.value === fieldSchema.default || forceUpdate)
|
((isVariable(fieldSchema.default) && field.value == null) || field.value === fieldSchema.default || forceUpdate)
|
||||||
) {
|
) {
|
||||||
// 一个变量字符串如果显示出来会比较奇怪
|
|
||||||
if (isVariable(field.value)) {
|
|
||||||
await field.reset({ forceClear: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
field.loading = true;
|
field.loading = true;
|
||||||
const collectionField = !fieldSchema.name.toString().includes('.') && collection?.getField(fieldSchema.name);
|
const collectionField = !fieldSchema.name.toString().includes('.') && collection?.getField(fieldSchema.name);
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import { useAttach, useComponent } from '../..';
|
|||||||
import { useApp } from '../../../application';
|
import { useApp } from '../../../application';
|
||||||
import { getCardItemSchema } from '../../../block-provider';
|
import { getCardItemSchema } from '../../../block-provider';
|
||||||
import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider';
|
import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider';
|
||||||
|
import { useDataBlockProps } from '../../../data-source';
|
||||||
import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider';
|
import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider';
|
||||||
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
||||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||||
@ -150,12 +151,15 @@ const WithForm = (props: WithFormProps) => {
|
|||||||
const linkageRules: any[] =
|
const linkageRules: any[] =
|
||||||
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
|
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
|
||||||
|
|
||||||
|
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
|
||||||
|
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = uid();
|
const id = uid();
|
||||||
|
|
||||||
form.addEffects(id, () => {
|
form.addEffects(id, () => {
|
||||||
onFormInputChange(() => {
|
onFormInputChange(() => {
|
||||||
setFormValueChanged?.(true);
|
setFormValueChanged?.(confirmBeforeClose);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -166,7 +170,7 @@ const WithForm = (props: WithFormProps) => {
|
|||||||
return () => {
|
return () => {
|
||||||
form.removeEffects(id);
|
form.removeEffects(id);
|
||||||
};
|
};
|
||||||
}, [form, props.disabled, setFormValueChanged]);
|
}, [form, props.disabled, setFormValueChanged, confirmBeforeClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -219,17 +223,19 @@ const WithForm = (props: WithFormProps) => {
|
|||||||
const WithoutForm = (props) => {
|
const WithoutForm = (props) => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { setFormValueChanged } = useActionContext();
|
const { setFormValueChanged } = useActionContext();
|
||||||
|
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
|
||||||
|
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
|
||||||
const form = useMemo(
|
const form = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createForm({
|
createForm({
|
||||||
disabled: props.disabled,
|
disabled: props.disabled,
|
||||||
effects() {
|
effects() {
|
||||||
onFormInputChange((form) => {
|
onFormInputChange((form) => {
|
||||||
setFormValueChanged?.(true);
|
setFormValueChanged?.(confirmBeforeClose);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[],
|
[confirmBeforeClose],
|
||||||
);
|
);
|
||||||
return fieldSchema['x-decorator'] === 'FormV2' ? (
|
return fieldSchema['x-decorator'] === 'FormV2' ? (
|
||||||
<FormDecorator form={form} {...props} />
|
<FormDecorator form={form} {...props} />
|
||||||
|
@ -158,7 +158,7 @@ const InternalList = withSkeletonComponent(
|
|||||||
>
|
>
|
||||||
<AntdList
|
<AntdList
|
||||||
{...props}
|
{...props}
|
||||||
pagination={!meta || !field.value?.length ? false : paginationProps}
|
pagination={!meta || !field.value?.length || count <= field.value?.length ? false : paginationProps}
|
||||||
loading={service?.loading}
|
loading={service?.loading}
|
||||||
>
|
>
|
||||||
{field.value?.length
|
{field.value?.length
|
||||||
|
@ -8,3 +8,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './List';
|
export * from './List';
|
||||||
|
export { useListBlockContext } from './List.Decorator';
|
||||||
|
@ -11,6 +11,7 @@ import { DeleteOutlined, DownloadOutlined, InboxOutlined, LoadingOutlined, PlusO
|
|||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
|
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
|
||||||
import { Alert, Upload as AntdUpload, Button, Modal, Progress, Space, Tooltip } from 'antd';
|
import { Alert, Upload as AntdUpload, Button, Modal, Progress, Space, Tooltip } from 'antd';
|
||||||
|
import { createGlobalStyle } from 'antd-style';
|
||||||
import useUploadStyle from 'antd/es/upload/style';
|
import useUploadStyle from 'antd/es/upload/style';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
@ -36,6 +37,12 @@ import {
|
|||||||
import { useStyles } from './style';
|
import { useStyles } from './style';
|
||||||
import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type';
|
import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type';
|
||||||
|
|
||||||
|
const LightBoxGlobalStyle = createGlobalStyle`
|
||||||
|
.ReactModal__Overlay.ReactModal__Overlay--after-open {
|
||||||
|
z-index: 3000 !important; // 避免预览图片时被遮挡
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
attachmentFileTypes.add({
|
attachmentFileTypes.add({
|
||||||
match(file) {
|
match(file) {
|
||||||
return matchMimetype(file, 'image/*');
|
return matchMimetype(file, 'image/*');
|
||||||
@ -62,6 +69,8 @@ attachmentFileTypes.add({
|
|||||||
[index, list],
|
[index, list],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<LightBoxGlobalStyle />
|
||||||
<LightBox
|
<LightBox
|
||||||
// discourageDownloads={true}
|
// discourageDownloads={true}
|
||||||
mainSrc={list[index]?.url}
|
mainSrc={list[index]?.url}
|
||||||
@ -85,6 +94,7 @@ attachmentFileTypes.add({
|
|||||||
</button>,
|
</button>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -398,9 +398,8 @@ export function Input(props: VariableInputProps) {
|
|||||||
const disabled = props.disabled || form.disabled;
|
const disabled = props.disabled || form.disabled;
|
||||||
|
|
||||||
return wrapSSR(
|
return wrapSSR(
|
||||||
|
<>
|
||||||
<Space.Compact style={style} className={classNames(componentCls, hashId, className)}>
|
<Space.Compact style={style} className={classNames(componentCls, hashId, className)}>
|
||||||
{/* 确保所有ant input样式都已加载 */}
|
|
||||||
<AntInput style={{ display: 'none' }} />
|
|
||||||
{variable ? (
|
{variable ? (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
@ -497,6 +496,9 @@ export function Input(props: VariableInputProps) {
|
|||||||
)}
|
)}
|
||||||
</Cascader>
|
</Cascader>
|
||||||
)}
|
)}
|
||||||
</Space.Compact>,
|
</Space.Compact>
|
||||||
|
{/* 确保所有ant input样式都已加载, 放到Compact中会导致Compact中的Input样式不对 */}
|
||||||
|
<AntInput style={{ display: 'none' }} />
|
||||||
|
</>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useRef, useState } from 'react';
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Button, Input } from 'antd';
|
import { Button, Input } from 'antd';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
import { VariableSelect } from './VariableSelect';
|
import { VariableSelect } from './VariableSelect';
|
||||||
|
|
||||||
// NOTE: https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js/46012210#46012210
|
// NOTE: https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js/46012210#46012210
|
||||||
@ -80,6 +80,7 @@ export function RawTextArea(props): JSX.Element {
|
|||||||
setOptions={setOptions}
|
setOptions={setOptions}
|
||||||
onInsert={onInsert}
|
onInsert={onInsert}
|
||||||
changeOnSelect={changeOnSelect}
|
changeOnSelect={changeOnSelect}
|
||||||
|
disabled={others.disabled}
|
||||||
/>
|
/>
|
||||||
</Button.Group>
|
</Button.Group>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import { Space, theme } from 'antd';
|
import { Input as AntInput, Space, theme } from 'antd';
|
||||||
import type { CascaderProps, DefaultOptionType } from 'antd/lib/cascader';
|
import type { CascaderProps, DefaultOptionType } from 'antd/lib/cascader';
|
||||||
import useInputStyle from 'antd/es/input/style';
|
import useInputStyle from 'antd/es/input/style';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@ -422,6 +422,7 @@ export function TextArea(props: TextAreaProps) {
|
|||||||
);
|
);
|
||||||
const disabled = props.disabled || form.disabled;
|
const disabled = props.disabled || form.disabled;
|
||||||
return wrapSSR(
|
return wrapSSR(
|
||||||
|
<>
|
||||||
<Space.Compact
|
<Space.Compact
|
||||||
className={cx(
|
className={cx(
|
||||||
componentCls,
|
componentCls,
|
||||||
@ -510,7 +511,10 @@ export function TextArea(props: TextAreaProps) {
|
|||||||
fieldNames={fieldNames || defaultFieldNames}
|
fieldNames={fieldNames || defaultFieldNames}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>,
|
</Space.Compact>
|
||||||
|
{/* 确保所有ant input样式都已加载, 放到Compact中会导致Compact中的Input样式不对 */}
|
||||||
|
<AntInput style={{ display: 'none' }} />
|
||||||
|
</>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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');
|
19
packages/plugins/@nocobase/plugin-departments/package.json
Normal file
19
packages/plugins/@nocobase/plugin-departments/package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"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.7.0-alpha.10",
|
||||||
|
"main": "dist/server/index.js",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nocobase/actions": "1.x",
|
||||||
|
"@nocobase/client": "1.x",
|
||||||
|
"@nocobase/server": "1.x",
|
||||||
|
"@nocobase/test": "1.x"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Users & permissions"
|
||||||
|
],
|
||||||
|
"gitHead": "ce89d10eec858c413f60e001e83c7c8cf2645f5e"
|
||||||
|
}
|
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,263 @@
|
|||||||
|
/**
|
||||||
|
* 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, TableProps } 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),
|
||||||
|
},
|
||||||
|
] as TableProps['columns']
|
||||||
|
}
|
||||||
|
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: ['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,281 @@
|
|||||||
|
/**
|
||||||
|
* 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 { UserDataResourceManager } from '@nocobase/plugin-user-data-sync';
|
||||||
|
import { MockDatabase, MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
import PluginUserDataSyncServer from '@nocobase/plugin-user-data-sync';
|
||||||
|
|
||||||
|
describe('department data sync', async () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: MockDatabase;
|
||||||
|
let resourceManager: UserDataResourceManager;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['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: 'user',
|
||||||
|
matchKey: 'email',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
customField: 'testField2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const department2 = await db.getRepository('users').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: ['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: ['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: ['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: ['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: ['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';
|
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