+
.x-button {
+ height: min-content;
}
`,
)}
- ref={inputRef}
- contentEditable={!disabled}
- dangerouslySetInnerHTML={{ __html: html }}
- />
-
- ,
+ >
+ {addonBefore && (
+
+ {addonBefore}
+
+ )}
+
+
+
+ {/* 确保所有ant input样式都已加载, 放到Compact中会导致Compact中的Input样式不对 */}
+
+ >,
);
}
diff --git a/packages/core/client/src/schema-component/common/utils/uitls.tsx b/packages/core/client/src/schema-component/common/utils/uitls.tsx
index 06d75c0c6b..ec2405dda5 100644
--- a/packages/core/client/src/schema-component/common/utils/uitls.tsx
+++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx
@@ -57,6 +57,7 @@ export const getTargetField = (obj) => {
}
});
const result = keys.slice(0, index);
+
return result;
};
@@ -76,78 +77,113 @@ function getAllKeys(obj) {
return keys;
}
+const parseVariableValue = async (targetVariable, variables, localVariables) => {
+ const parsingResult = isVariable(targetVariable)
+ ? [variables.parseVariable(targetVariable, localVariables).then(({ value }) => value)]
+ : [targetVariable];
+
+ try {
+ const [value] = await Promise.all(parsingResult);
+ return value;
+ } catch (error) {
+ console.error('Error in parseVariableValue:', error);
+ throw error;
+ }
+};
+
export const conditionAnalyses = async (
{
ruleGroup,
variables,
localVariables,
variableNameOfLeftCondition,
+ conditionType,
}: {
ruleGroup;
variables: VariablesContextType;
localVariables: VariableOption[];
- /**
- * used to parse the variable name of the left condition value
- * @default '$nForm'
- */
variableNameOfLeftCondition?: string;
+ conditionType?: 'advanced' | 'basic';
},
jsonLogic: any,
) => {
const type = Object.keys(ruleGroup)[0] || '$and';
const conditions = ruleGroup[type];
- let results = conditions.map(async (condition) => {
- if ('$and' in condition || '$or' in condition) {
- return await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic);
- }
-
- const logicCalculation = getInnermostKeyAndValue(condition);
- const operator = logicCalculation?.key;
-
- if (!operator) {
- return true;
- }
-
- const targetVariableName = targetFieldToVariableString(getTargetField(condition), variableNameOfLeftCondition);
- const targetValue = variables
- .parseVariable(targetVariableName, localVariables, {
- doNotRequest: true,
- })
- .then(({ value }) => value);
-
- const parsingResult = isVariable(logicCalculation?.value)
- ? [variables.parseVariable(logicCalculation?.value, localVariables).then(({ value }) => value), targetValue]
- : [logicCalculation?.value, targetValue];
-
- try {
- const [value, targetValue] = await Promise.all(parsingResult);
- const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables);
- let currentInputValue = transformVariableValue(targetValue, { targetCollectionField });
- const comparisonValue = transformVariableValue(value, { targetCollectionField });
- if (
- targetCollectionField?.type &&
- ['datetime', 'date', 'datetimeNoTz', 'dateOnly', 'unixTimestamp'].includes(targetCollectionField.type) &&
- currentInputValue
- ) {
- const picker = inferPickerType(comparisonValue);
- const format = getPickerFormat(picker);
- currentInputValue = dayjs(currentInputValue).format(format);
- }
-
- return jsonLogic.apply({
- [operator]: [currentInputValue, comparisonValue],
- });
- } catch (error) {
- throw error;
- }
- });
- results = await Promise.all(results);
+ const results = await Promise.all(
+ conditions.map((condition) =>
+ processCondition(condition, variables, localVariables, variableNameOfLeftCondition, conditionType, jsonLogic),
+ ),
+ );
if (type === '$and') {
return every(results, (v) => v);
} else {
- return some(results, (v) => v);
+ if (results.length) {
+ return some(results, (v) => v);
+ }
+ return true;
+ }
+};
+
+const processCondition = async (
+ condition,
+ variables,
+ localVariables,
+ variableNameOfLeftCondition,
+ conditionType,
+ jsonLogic,
+) => {
+ if ('$and' in condition || '$or' in condition) {
+ return await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic);
+ }
+ return conditionType === 'advanced'
+ ? processAdvancedCondition(condition, variables, localVariables, jsonLogic)
+ : processBasicCondition(condition, variables, localVariables, variableNameOfLeftCondition, jsonLogic);
+};
+
+const processAdvancedCondition = async (condition, variables, localVariables, jsonLogic) => {
+ const operator = condition.op;
+ const rightValue = await parseVariableValue(condition.rightVar, variables, localVariables);
+ const leftValue = await parseVariableValue(condition.leftVar, variables, localVariables);
+ if (operator) {
+ return jsonLogic.apply({ [operator]: [leftValue, rightValue] });
+ }
+ return true;
+};
+
+const processBasicCondition = async (condition, variables, localVariables, variableNameOfLeftCondition, jsonLogic) => {
+ const logicCalculation = getInnermostKeyAndValue(condition);
+ const operator = logicCalculation?.key;
+ if (!operator) return true;
+
+ const targetVariableName = targetFieldToVariableString(getTargetField(condition), variableNameOfLeftCondition);
+ const targetValue = variables
+ .parseVariable(targetVariableName, localVariables, { doNotRequest: true })
+ .then(({ value }) => value);
+
+ const parsingResult = isVariable(logicCalculation?.value)
+ ? [variables.parseVariable(logicCalculation?.value, localVariables).then(({ value }) => value), targetValue]
+ : [logicCalculation?.value, targetValue];
+
+ try {
+ const [value, resolvedTargetValue] = await Promise.all(parsingResult);
+ const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables);
+ let currentInputValue = transformVariableValue(resolvedTargetValue, { targetCollectionField });
+ const comparisonValue = transformVariableValue(value, { targetCollectionField });
+
+ if (
+ targetCollectionField?.type &&
+ ['datetime', 'date', 'datetimeNoTz', 'dateOnly', 'unixTimestamp'].includes(targetCollectionField.type) &&
+ currentInputValue
+ ) {
+ const picker = inferPickerType(comparisonValue);
+ const format = getPickerFormat(picker);
+ currentInputValue = dayjs(currentInputValue).format(format);
+ }
+ return jsonLogic.apply({ [operator]: [currentInputValue, comparisonValue] });
+ } catch (error) {
+ throw error;
}
};
diff --git a/packages/core/client/src/schema-component/core/DesignableSwitch.tsx b/packages/core/client/src/schema-component/core/DesignableSwitch.tsx
index b6f86142b4..245460c717 100644
--- a/packages/core/client/src/schema-component/core/DesignableSwitch.tsx
+++ b/packages/core/client/src/schema-component/core/DesignableSwitch.tsx
@@ -9,7 +9,7 @@
import { HighlightOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
-import React from 'react';
+import React, { FC } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { useDesignable } from '..';
@@ -24,7 +24,7 @@ const unDesignableStyle = {
backgroundColor: 'transparent',
};
-export const DesignableSwitch = () => {
+export const DesignableSwitch: FC<{ style?: React.CSSProperties }> = (props) => {
const { designable, setDesignable } = useDesignable();
const { t } = useTranslation();
const { token } = useToken();
@@ -46,7 +46,7 @@ export const DesignableSwitch = () => {
icon={
}
title={t('UI Editor')}
// subtitle={'Ctrl+Shift+U'}
- style={style}
+ style={{ ...style, ...props.style }}
onClick={() => {
setDesignable(!designable);
}}
diff --git a/packages/core/client/src/schema-component/hooks/useDesignable.tsx b/packages/core/client/src/schema-component/hooks/useDesignable.tsx
index ed5e7f82d4..8fc4fddec1 100644
--- a/packages/core/client/src/schema-component/hooks/useDesignable.tsx
+++ b/packages/core/client/src/schema-component/hooks/useDesignable.tsx
@@ -799,7 +799,7 @@ export function useDesignable() {
return component;
}
const c = get(components, component);
- return c[LAZY_COMPONENT_KEY] ?? c;
+ return c?.[LAZY_COMPONENT_KEY] ?? c;
},
[get],
),
diff --git a/packages/core/client/src/schema-component/hooks/useFieldModeOptions.tsx b/packages/core/client/src/schema-component/hooks/useFieldModeOptions.tsx
index e5ca454562..4e2c411042 100644
--- a/packages/core/client/src/schema-component/hooks/useFieldModeOptions.tsx
+++ b/packages/core/client/src/schema-component/hooks/useFieldModeOptions.tsx
@@ -31,12 +31,7 @@ export const useFieldModeOptions = (props?) => {
if (!collectionField || !collectionField?.interface) {
return;
}
- if (
- !['o2o', 'oho', 'obo', 'o2m', 'linkTo', 'm2o', 'm2m', 'updatedBy', 'createdBy', 'mbm', 'attachmentURL'].includes(
- collectionField.interface,
- )
- )
- return;
+ if (!['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type)) return;
const collection = getCollection(collectionField.target);
if (collection?.template === 'file') {
return isReadPretty
diff --git a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx
index 1cde756d2e..627aeaf7e3 100644
--- a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx
+++ b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx
@@ -89,6 +89,7 @@ const InternalCreateRecordAction = (props: any, ref) => {
condition: v.condition,
variables,
localVariables,
+ conditionType: v.conditionType,
},
app.jsonLogic,
);
@@ -208,6 +209,7 @@ export const CreateAction = observer(
condition: v.condition,
variables,
localVariables,
+ conditionType: v.conditionType,
},
app.jsonLogic,
);
diff --git a/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx b/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx
index b1bf08c57e..c17d095413 100644
--- a/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx
+++ b/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx
@@ -26,7 +26,7 @@ import { VariableInput, getShouldChange } from '../../../schema-settings/Variabl
import { Option } from '../../../schema-settings/VariableInput/type';
import { formatVariableScop } from '../../../schema-settings/VariableInput/utils/formatVariableScop';
import { useLocalVariables, useVariables } from '../../../variables';
-
+import { BlockContext, useBlockContext } from '../../../block-provider';
interface AssignedFieldProps {
value: any;
onChange: (value: any) => void;
@@ -93,7 +93,7 @@ export enum AssignedFieldValueType {
DynamicValue = 'dynamicValue',
}
-export const AssignedField = (props: AssignedFieldProps) => {
+export const AssignedFieldInner = (props: AssignedFieldProps) => {
const { value, onChange } = props;
const { getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager_deprecated();
const collection = useCollection_deprecated();
@@ -148,3 +148,13 @@ export const AssignedField = (props: AssignedFieldProps) => {
/>
);
};
+
+export const AssignedField = (props) => {
+ const { form } = useFormBlockContext();
+ const { name } = useBlockContext() || {};
+ return (
+
+
+
+ );
+};
diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts
index ac86cd5a75..60e3b34758 100644
--- a/packages/core/client/src/schema-initializer/utils.ts
+++ b/packages/core/client/src/schema-initializer/utils.ts
@@ -471,7 +471,6 @@ export const useFilterFormItemInitializerFields = (options?: any) => {
'x-collection-field': `${name}.${field.name}`,
'x-component-props': {
utc: false,
- underFilter: true,
},
};
if (isAssocField(field)) {
@@ -486,7 +485,7 @@ export const useFilterFormItemInitializerFields = (options?: any) => {
'x-decorator': 'FormItem',
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': `${name}.${field.name}`,
- 'x-component-props': { ...field.uiSchema?.['x-component-props'], utc: false, underFilter: true },
+ 'x-component-props': { ...field.uiSchema?.['x-component-props'], utc: false },
};
}
const resultItem = {
@@ -581,7 +580,7 @@ const associationFieldToMenu = (
interface: field.interface,
},
'x-component': 'CollectionField',
- 'x-component-props': { utc: false, underFilter: true },
+ 'x-component-props': { utc: false },
'x-read-pretty': false,
'x-decorator': 'FormItem',
'x-collection-field': `${collectionName}.${schemaName}`,
@@ -698,7 +697,7 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
- 'x-component-props': { utc: false, underFilter: true },
+ 'x-component-props': { utc: false },
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
};
return {
@@ -729,7 +728,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => {
const remove = useRemoveGridFormItem();
return currentFields
?.filter((field) => {
- return field?.interface && field.interface !== 'snapshot' && field.type !== 'sequence';
+ return !field.inherit && field?.interface && field.interface !== 'snapshot' && field.type !== 'sequence';
})
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
@@ -769,7 +768,7 @@ export const findSchema = (schema: Schema, key: string, action: string, name?: s
if (s[key] === action && (!name || s.name === name)) {
return s;
}
- if (s['x-component'] !== 'Action.Container' && !s['x-component'].includes('AssociationField')) {
+ if (s['x-component'] && s['x-component'] !== 'Action.Container' && !s['x-component'].includes('AssociationField')) {
const c = findSchema(s, key, action, name);
if (c) {
return c;
diff --git a/packages/core/client/src/schema-settings/LinkageRules/ValueDynamicComponent.tsx b/packages/core/client/src/schema-settings/LinkageRules/ValueDynamicComponent.tsx
index 21b74f960b..5b26a84eac 100644
--- a/packages/core/client/src/schema-settings/LinkageRules/ValueDynamicComponent.tsx
+++ b/packages/core/client/src/schema-settings/LinkageRules/ValueDynamicComponent.tsx
@@ -97,6 +97,17 @@ export const ValueDynamicComponent = (props: ValueDynamicComponentProps) => {
.ant-input-affix-wrapper {
border-radius: 0px;
}
+ .ant-checkbox-wrapper {
+ margin-left: 50%;
+ }
+ .ant-select-selector {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ .ant-picker {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
`}
>
{React.createElement(DynamicComponent, {
diff --git a/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts b/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts
index 6ae599c86e..2a3cbbeb52 100644
--- a/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts
+++ b/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts
@@ -38,6 +38,7 @@ interface Props {
*/
variableNameOfLeftCondition?: string;
action?: any;
+ conditionType?: 'advanced' | 'basic';
}
export function bindLinkageRulesToFiled(
@@ -83,7 +84,6 @@ export function bindLinkageRulesToFiled(
() => {
// 获取条件中的字段值
const fieldValuesInCondition = getFieldValuesInCondition({ linkageRules, formValues });
-
// 获取条件中的变量值
const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables });
@@ -132,20 +132,37 @@ function getVariableValuesInCondition({
return linkageRules.map((rule) => {
const type = Object.keys(rule.condition)[0] || '$and';
const conditions = rule.condition[type];
+ if (rule.conditionType === 'advanced') {
+ return conditions
+ .map((condition) => {
+ if (!condition) {
+ return null;
+ }
- return conditions
- .map((condition) => {
- const jsonlogic = getInnermostKeyAndValue(condition);
- if (!jsonlogic) {
- return null;
- }
- if (isVariable(jsonlogic.value)) {
- return getVariableValue(jsonlogic.value, localVariables);
- }
+ const resolveVariable = (varName) =>
+ isVariable(varName) ? getVariableValue(varName, localVariables) : varName;
- return jsonlogic.value;
- })
- .filter(Boolean);
+ return {
+ leftVar: resolveVariable(condition.leftVar),
+ rightVar: resolveVariable(condition.rightVar),
+ };
+ })
+ .filter(Boolean);
+ } else {
+ return conditions
+ .map((condition) => {
+ const jsonlogic = getInnermostKeyAndValue(condition);
+ if (!jsonlogic) {
+ return null;
+ }
+ if (isVariable(jsonlogic.value)) {
+ return getVariableValue(jsonlogic.value, localVariables);
+ }
+
+ return jsonlogic.value;
+ })
+ .filter(Boolean);
+ }
});
}
@@ -216,6 +233,7 @@ function getSubscriber(
localVariables,
variableNameOfLeftCondition,
action,
+ conditionType: rule.conditionType,
},
jsonLogic,
);
@@ -256,6 +274,13 @@ function getSubscriber(
// 在 FormItem 中有使用这个属性来判断字段是否被隐藏
field.data.hidden = true;
+ // 如果字段是必填的,并且被隐藏(保留值)了,那么就需要把 required 设置为 false,否则有可能会导致表单验证失败;
+ // 进而导致点击提交按钮无效的问题。
+ if (field.required) {
+ field.required = false;
+ field.data.prevRequired = true;
+ }
+
requestAnimationFrame(() => {
field.setState((state) => {
state.display = 'visible';
@@ -277,6 +302,13 @@ function getSubscriber(
field.data = field.data || {};
// 在 FormItem 中有使用这个属性来判断字段是否被隐藏
field.data.hidden = false;
+
+ // 当“隐藏(保留值)”的字段再次显示时,恢复“必填”的状态
+ if (fieldName === 'display' && lastState?.value === 'visible' && field.data.prevRequired) {
+ delete field.data.prevRequired;
+ field.required = true;
+ }
+
requestAnimationFrame(() => {
field.setState((state) => {
state[fieldName] = lastState?.value;
@@ -327,7 +359,17 @@ function getFieldNameByOperator(operator: ActionType) {
}
export const collectFieldStateOfLinkageRules = (
- { operator, value, field, condition, variables, localVariables, variableNameOfLeftCondition, action }: Props,
+ {
+ operator,
+ value,
+ field,
+ condition,
+ variables,
+ localVariables,
+ variableNameOfLeftCondition,
+ action,
+ conditionType,
+ }: Props,
jsonLogic: any,
) => {
const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required];
@@ -336,7 +378,13 @@ export const collectFieldStateOfLinkageRules = (
const valueResult = field?.stateOfLinkageRules?.value || [field?.initStateOfLinkageRules?.value];
const optionsResult = field?.stateOfLinkageRules?.dataSource || [field?.initStateOfLinkageRules?.dataSource];
const { evaluate } = evaluators.get('formula.js');
- const paramsToGetConditionResult = { ruleGroup: condition, variables, localVariables, variableNameOfLeftCondition };
+ const paramsToGetConditionResult = {
+ ruleGroup: condition,
+ variables,
+ localVariables,
+ variableNameOfLeftCondition,
+ conditionType,
+ };
const dateScopeResult = field?.stateOfLinkageRules?.dateScope || [field?.initStateOfLinkageRules?.dateScope];
switch (operator) {
diff --git a/packages/core/client/src/schema-settings/LinkageRules/compute-rules.ts b/packages/core/client/src/schema-settings/LinkageRules/compute-rules.ts
index 12ca65e77f..ffece9a6d3 100644
--- a/packages/core/client/src/schema-settings/LinkageRules/compute-rules.ts
+++ b/packages/core/client/src/schema-settings/LinkageRules/compute-rules.ts
@@ -38,7 +38,12 @@ const getSatisfiedActions = async ({ rules, variables, localVariables }, jsonLog
rules
.filter((k) => !k.disabled)
.map(async (rule) => {
- if (await conditionAnalyses({ ruleGroup: rule.condition, variables, localVariables }, jsonLogic)) {
+ if (
+ await conditionAnalyses(
+ { ruleGroup: rule.condition, variables, localVariables, conditionType: rule.conditionType },
+ jsonLogic,
+ )
+ ) {
return rule;
} else return null;
}),
diff --git a/packages/core/client/src/schema-settings/LinkageRules/index.tsx b/packages/core/client/src/schema-settings/LinkageRules/index.tsx
index c5b97bfb27..5988ff898e 100644
--- a/packages/core/client/src/schema-settings/LinkageRules/index.tsx
+++ b/packages/core/client/src/schema-settings/LinkageRules/index.tsx
@@ -10,7 +10,7 @@
import { css } from '@emotion/css';
import { observer, useFieldSchema } from '@formily/react';
import React, { useMemo } from 'react';
-import { useCollectionManager_deprecated } from '../../collection-manager';
+import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager';
import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider';
import { CollectionProvider } from '../../data-source/collection/CollectionProvider';
import { withDynamicSchemaProps } from '../../hoc/withDynamicSchemaProps';
@@ -20,30 +20,95 @@ import { SubFormProvider } from '../../schema-component/antd/association-field/h
import { DynamicComponentProps } from '../../schema-component/antd/filter/DynamicComponent';
import { FilterContext } from '../../schema-component/antd/filter/context';
import { VariableInput, getShouldChange } from '../VariableInput/VariableInput';
+import { useCurrentFormContext } from '../VariableInput/hooks/useFormVariable';
import { LinkageRuleActionGroup } from './LinkageRuleActionGroup';
import { EnableLinkage } from './components/EnableLinkage';
import { ArrayCollapse } from './components/LinkageHeader';
+import { useFlag } from '../../flag-provider';
export interface Props {
dynamicComponent: any;
}
+function extractFieldPath(obj, path = []) {
+ if (typeof obj !== 'object' || obj === null) return null;
+
+ const [key, value] = Object.entries(obj)[0] || [];
+
+ if (typeof value === 'object' && value !== null && !key.startsWith('$')) {
+ return extractFieldPath(value, [...path, key]);
+ }
+
+ return [path.join('.'), obj];
+}
+type Condition = { [field: string]: { [op: string]: any } } | { $and: Condition[] } | { $or: Condition[] };
+type TransformedCondition =
+ | { leftVar: string; op: string; rightVar: any }
+ | { $and: TransformedCondition[] }
+ | { $or: TransformedCondition[] };
+function transformConditionData(condition: Condition, variableKey: '$nForm' | '$nRecord'): TransformedCondition {
+ if ('$and' in condition) {
+ return {
+ $and: condition.$and.map((c) => transformConditionData(c, variableKey)),
+ };
+ }
+
+ if ('$or' in condition) {
+ return {
+ $or: condition.$or.map((c) => transformConditionData(c, variableKey)),
+ };
+ }
+ const [field, expression] = extractFieldPath(condition || {}) || [];
+
+ const [op, value] = Object.entries(expression || {})[0] || [];
+ return {
+ leftVar: field ? `{{${variableKey}.${field}}}` : null,
+ op,
+ rightVar: value,
+ };
+}
+function getActiveContextName(underNester, shouldDisplayCurrentForm): string | null {
+ if (underNester) {
+ return '$iteration';
+ }
+ if (shouldDisplayCurrentForm) {
+ return '$nForm';
+ }
+ return '$nRecord';
+}
+
+const transformDefaultValue = (values, variableKey) => {
+ return values.map((v) => {
+ if (v.conditionType !== 'advanced') {
+ const condition = transformConditionData(v.condition, variableKey);
+ return {
+ ...v,
+ condition: variableKey ? condition : v.condition,
+ conditionType: variableKey ? 'advanced' : 'basic',
+ };
+ }
+ return v;
+ });
+};
export const FormLinkageRules = withDynamicSchemaProps(
observer((props: Props) => {
const fieldSchema = useFieldSchema();
const { options, defaultValues, collectionName, form, variables, localVariables, record, dynamicComponent } =
useProps(props); // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
+ const { name } = useCollection_deprecated();
const { getAllCollectionsInheritChain } = useCollectionManager_deprecated();
const parentRecordData = useCollectionParentRecordData();
-
+ const { shouldDisplayCurrentForm } = useCurrentFormContext();
const components = useMemo(() => ({ ArrayCollapse }), []);
+ const { isInSubTable, isInSubForm } = useFlag();
+ const variableKey = getActiveContextName(isInSubTable || isInSubForm, shouldDisplayCurrentForm);
const schema = useMemo(
() => ({
type: 'object',
properties: {
rules: {
type: 'array',
- default: defaultValues,
+ default: transformDefaultValue(defaultValues, variableKey),
'x-component': 'ArrayCollapse',
'x-decorator': 'FormItem',
'x-component-props': {
@@ -72,6 +137,20 @@ export const FormLinkageRules = withDynamicSchemaProps(
'x-content': '{{ t("Condition") }}',
},
condition: {
+ 'x-component': 'Input', // 仅作为数据存储
+ 'x-hidden': true, // 不显示
+ 'x-reactions': [
+ {
+ dependencies: ['.conditionType', '.conditionBasic', '.conditionAdvanced'],
+ fulfill: {
+ state: {
+ value: '{{$deps[0] === "basic" ? $deps[1] : $deps[2]}}',
+ },
+ },
+ },
+ ],
+ },
+ conditionBasic: {
'x-component': 'Filter',
'x-use-component-props': () => {
return {
@@ -83,6 +162,7 @@ export const FormLinkageRules = withDynamicSchemaProps(
`,
};
},
+ 'x-visible': '{{$deps[0] === "basic"}}',
'x-component-props': {
collectionName,
dynamicComponent: (props: DynamicComponentProps) => {
@@ -102,6 +182,38 @@ export const FormLinkageRules = withDynamicSchemaProps(
);
},
},
+ 'x-reactions': [
+ {
+ dependencies: ['.conditionType', '.condition'],
+ fulfill: {
+ state: {
+ visible: '{{$deps[0] === "basic"}}',
+ value: '{{$deps[0] === "basic" ? $deps[1] : undefined}}',
+ },
+ },
+ },
+ ],
+ },
+ conditionAdvanced: {
+ 'x-component': 'LinkageFilter',
+ 'x-visible': '{{$deps[0] === "advanced"}}',
+ 'x-reactions': [
+ {
+ dependencies: ['.conditionType', '.condition'],
+ fulfill: {
+ state: {
+ visible: '{{$deps[0] === "advanced"}}',
+ value: '{{$deps[0] === "advanced" ? $deps[1] : undefined}}',
+ },
+ },
+ },
+ ],
+ },
+ conditionType: {
+ type: 'string',
+ 'x-component': 'Input',
+ default: 'advanced',
+ 'x-hidden': true,
},
actions: {
'x-component': 'h4',
@@ -168,10 +280,10 @@ export const FormLinkageRules = withDynamicSchemaProps(
return (
// 这里使用 SubFormProvider 包裹,是为了让子表格的联动规则中 “当前对象” 的配置显示正确
-
+
-
+
diff --git a/packages/core/client/src/schema-settings/LinkageRules/type.ts b/packages/core/client/src/schema-settings/LinkageRules/type.ts
index 1f59a6a849..05abd99617 100644
--- a/packages/core/client/src/schema-settings/LinkageRules/type.ts
+++ b/packages/core/client/src/schema-settings/LinkageRules/type.ts
@@ -32,9 +32,11 @@ export enum ActionType {
export enum LinkageRuleCategory {
default = 'default',
style = 'style',
+ button = 'button',
}
export const LinkageRuleDataKeyMap: Record<`${LinkageRuleCategory}`, string> = {
[LinkageRuleCategory.style]: 'x-linkage-style-rules',
[LinkageRuleCategory.default]: 'x-linkage-rules',
+ [LinkageRuleCategory.button]: 'x-linkage-rules',
};
diff --git a/packages/core/client/src/schema-settings/LinkageRules/useValues.ts b/packages/core/client/src/schema-settings/LinkageRules/useValues.ts
index 0001622468..5a86fafcd0 100644
--- a/packages/core/client/src/schema-settings/LinkageRules/useValues.ts
+++ b/packages/core/client/src/schema-settings/LinkageRules/useValues.ts
@@ -34,7 +34,37 @@ export const useValues = (options) => {
const dataIndex = field.data?.targetFields;
const option = (dataIndex && findOption(dataIndex, options)) || {};
const operators = option?.operators || [];
- field.data.operators = operators;
+ field.data.operators = operators?.filter((v) => {
+ const isOptionField = ['select', 'radioGroup', 'multipleSelect', 'checkboxGroup'].includes(
+ option?.interface || '',
+ );
+ const isDateField = [
+ 'date',
+ 'datetime',
+ 'dateOnly',
+ 'datetimeNoTz',
+ 'unixTimestamp',
+ 'createdAt',
+ 'updatedAt',
+ ].includes(option?.interface || '');
+
+ // 如果 多个字段,则排除 Value、DateScope、Options
+ if (dataIndex.length > 1 && [ActionType.Value, ActionType.DateScope, ActionType.Options].includes(v.value)) {
+ return false;
+ }
+
+ // 非选项字段,去掉 Options
+ if (!isOptionField && v.value === ActionType.Options) {
+ return false;
+ }
+
+ // 非时间字段,去掉 DateScope
+ if (!isDateField && v.value === ActionType.DateScope) {
+ return false;
+ }
+
+ return true;
+ });
field.data.schema = option?.schema;
};
useEffect(value2data, [logic]);
diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx
index 0cf7546b09..c277f087b7 100644
--- a/packages/core/client/src/schema-settings/SchemaSettings.tsx
+++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx
@@ -84,7 +84,7 @@ import { AssociationOrCollectionProvider, useDataBlockProps } from '../data-sour
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider';
import { useFilterBlock } from '../filter-provider/FilterProvider';
-import { FlagProvider } from '../flag-provider';
+import { FlagProvider, useFlag } from '../flag-provider';
import { useGlobalTheme } from '../global-theme';
import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem';
import {
@@ -96,6 +96,7 @@ import { useRecord } from '../record-provider';
import { ActionContextProvider } from '../schema-component/antd/action/context';
import { SubFormProvider, useSubFormValue } from '../schema-component/antd/association-field/hooks';
import { FormDialog } from '../schema-component/antd/form-dialog';
+import { AllDataBlocksContext } from '../schema-component/antd/page/AllDataBlocksProvider';
import { SchemaComponentContext } from '../schema-component/context';
import { FormProvider } from '../schema-component/core/FormProvider';
import { RemoteSchemaComponent } from '../schema-component/core/RemoteSchemaComponent';
@@ -568,13 +569,14 @@ export interface SchemaSettingsSelectItemProps
extends Omit,
Omit {
value?: SelectWithTitleProps['defaultValue'];
+ optionRender?: (option: any, info: { index: number }) => React.ReactNode;
}
export const SchemaSettingsSelectItem: FC = (props) => {
- const { title, options, value, onChange, ...others } = props;
+ const { title, options, value, onChange, optionRender, ...others } = props;
return (
-
+
);
};
@@ -815,6 +817,7 @@ export interface SchemaSettingsModalItemProps {
noRecord?: boolean;
/** 自定义 Modal 上下文 */
ModalContextProvider?: React.FC;
+ dialogRootClassName?: string;
}
export const SchemaSettingsModalItem: FC = (props) => {
const {
@@ -829,6 +832,7 @@ export const SchemaSettingsModalItem: FC = (props)
width = 'fit-content',
noRecord = false,
ModalContextProvider = (props) => <>{props.children}>,
+ dialogRootClassName,
...others
} = props;
const options = useContext(SchemaOptionsContext);
@@ -849,6 +853,7 @@ export const SchemaSettingsModalItem: FC = (props)
const { getOperators } = useOperators();
const locationSearch = useLocationSearch();
const variableOptions = useVariables();
+ const allDataBlocks = useContext(AllDataBlocksContext);
// 解决变量`当前对象`值在弹窗中丢失的问题
const { formValue: subFormValue, collection: subFormCollection, parent } = useSubFormValue();
@@ -869,79 +874,87 @@ export const SchemaSettingsModalItem: FC = (props)
const values = asyncGetInitialValues ? await asyncGetInitialValues() : initialValues;
const schema = _.isFunction(props.schema) ? props.schema() : props.schema;
FormDialog(
- { title: schema.title || title, width },
+ { title: schema.title || title, width, rootClassName: dialogRootClassName },
() => {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- 576px
- @media (min-width: 576px) {
- min-width: 520px;
- }
-
- // screen <= 576px
- @media (max-width: 576px) {
- min-width: 320px;
- }
- `}
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ 576px
+ @media (min-width: 576px) {
+ min-width: 520px;
+ }
+
+ // screen <= 576px
+ @media (max-width: 576px) {
+ min-width: 320px;
+ }
+ `}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
},
theme,
@@ -1115,7 +1128,8 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) {
const getRules = useCallback(() => {
return gridSchema?.[dataKey] || fieldSchema?.[dataKey] || [];
}, [gridSchema, fieldSchema, dataKey]);
- const title = titleMap[category];
+ const title = titleMap[category] || t('Linkage rules');
+ const flagVales = useFlag();
const schema = useMemo(
() => ({
type: 'object',
@@ -1148,7 +1162,7 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) {
(v) => {
const rules = [];
for (const rule of v.fieldReaction.rules) {
- rules.push(_.pickBy(rule, _.identity));
+ rules.push(_.omit(_.pickBy(rule, _.identity), ['conditionBasic', 'conditionAdvanced']));
}
const templateId = gridSchema['x-component'] === 'BlockTemplate' && gridSchema['x-component-props']?.templateId;
const uid = (templateId && getTemplateById(templateId).uid) || gridSchema['x-uid'];
@@ -1167,7 +1181,16 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) {
);
return (
-
+ {
+ return {props.children} ;
+ }}
+ />
);
};
diff --git a/packages/core/client/src/schema-settings/SchemaSettingsConnectDataBlocks.tsx b/packages/core/client/src/schema-settings/SchemaSettingsConnectDataBlocks.tsx
index c0a28ac786..463e78bef8 100644
--- a/packages/core/client/src/schema-settings/SchemaSettingsConnectDataBlocks.tsx
+++ b/packages/core/client/src/schema-settings/SchemaSettingsConnectDataBlocks.tsx
@@ -52,24 +52,10 @@ export function SchemaSettingsConnectDataBlocks(props) {
const Content = dataBlocks.map((block) => {
const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`;
const onHover = () => {
- const dom = block.dom;
- const designer = dom.querySelector('.general-schema-designer') as HTMLElement;
- if (designer) {
- designer.style.display = 'block';
- }
- dom.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.2)';
- dom.scrollIntoView({
- behavior: 'smooth',
- block: 'center',
- });
+ block.highlightBlock();
};
const onLeave = () => {
- const dom = block.dom;
- const designer = dom.querySelector('.general-schema-designer') as HTMLElement;
- if (designer) {
- designer.style.display = null;
- }
- dom.style.boxShadow = 'none';
+ block.unhighlightBlock();
};
if (isSameCollection(block.collection, collection)) {
return (
diff --git a/packages/core/client/src/schema-settings/SchemaSettingsLayoutItem.tsx b/packages/core/client/src/schema-settings/SchemaSettingsLayoutItem.tsx
index dd93fdf03f..86753306aa 100644
--- a/packages/core/client/src/schema-settings/SchemaSettingsLayoutItem.tsx
+++ b/packages/core/client/src/schema-settings/SchemaSettingsLayoutItem.tsx
@@ -107,20 +107,30 @@ export const SchemaSettingsLayoutItem = function LayoutItem() {
},
},
},
+ colon: {
+ type: 'boolean',
+ 'x-content': t('Colon'),
+ required: true,
+ default: fieldSchema?.['x-component-props']?.colon !== false,
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ },
},
} as ISchema
}
- onSubmit={({ layout, labelAlign, labelWidth, labelWrap }) => {
+ onSubmit={({ layout, labelAlign, labelWidth, labelWrap, colon }) => {
const componentProps = fieldSchema['x-component-props'] || {};
componentProps.layout = layout;
componentProps.labelAlign = labelAlign;
componentProps.labelWidth = layout === 'horizontal' ? labelWidth : null;
componentProps.labelWrap = labelWrap;
+ componentProps.colon = colon;
fieldSchema['x-component-props'] = componentProps;
field.componentProps.layout = layout;
field.componentProps.labelAlign = labelAlign;
field.componentProps.labelWidth = labelWidth;
field.componentProps.labelWrap = labelWrap;
+ field.componentProps.colon = colon;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
diff --git a/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx b/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx
index 0122fde57e..6c2b593616 100644
--- a/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx
+++ b/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx
@@ -11,7 +11,7 @@ import { Form } from '@formily/core';
// @ts-ignore
import { Schema } from '@formily/json-schema';
import _ from 'lodash';
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions_deprecated } from '../../collection-manager';
import { Variable, useVariableScope } from '../../schema-component';
@@ -91,6 +91,10 @@ type Props = {
*/
noDisabled?: boolean;
hideVariableButton?: boolean;
+ setScopes?: any; //更新scopes
+ nullable?: boolean;
+ constantAbel?: boolean;
+ changeOnSelect?: boolean;
};
/**
@@ -117,6 +121,10 @@ export const VariableInput = (props: Props) => {
targetFieldSchema,
noDisabled,
hideVariableButton,
+ setScopes,
+ nullable = true,
+ constantAbel = true,
+ changeOnSelect = true,
} = props;
const { name: blockCollectionName } = useBlockCollection();
const scope = useVariableScope();
@@ -146,31 +154,37 @@ export const VariableInput = (props: Props) => {
const handleChange = useCallback(
(value: any, optionPath: any[]) => {
if (!shouldChange) {
- return onChange(value);
+ return onChange(value, optionPath);
}
// `shouldChange` 这个函数的运算量比较大,会导致展开变量列表时有明显的卡顿感,在这里加个延迟能有效解决这个问题
setTimeout(async () => {
if (await shouldChange(value, optionPath)) {
- onChange(value);
+ onChange(value, optionPath);
}
});
},
[onChange, shouldChange],
);
+ const scopes = returnScope(
+ compatOldVariables(_.isEmpty(scope) ? variableOptions : scope, {
+ value,
+ }),
+ );
+ useEffect(() => {
+ setScopes?.(scopes);
+ }, [value, scope]);
return (
diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/index.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/index.ts
index ec9cd6da7f..88bb03b409 100644
--- a/packages/core/client/src/schema-settings/VariableInput/hooks/index.ts
+++ b/packages/core/client/src/schema-settings/VariableInput/hooks/index.ts
@@ -15,3 +15,4 @@ export * from './useURLSearchParamsVariable';
export * from './useUserVariable';
export * from './useVariableOptions';
export * from './usePopupVariable';
+export * from './useContextAssociationFields';
diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useAPITokenVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useAPITokenVariable.ts
index 3538a7d746..eac0d1dc6b 100644
--- a/packages/core/client/src/schema-settings/VariableInput/hooks/useAPITokenVariable.ts
+++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useAPITokenVariable.ts
@@ -9,6 +9,7 @@
import { useAPIClient } from '../../../api-client/hooks/useAPIClient';
import { useBaseVariable } from './useBaseVariable';
+import { string } from '../../../collection-manager/interfaces/properties/operators';
/**
* 变量:`当前 Token`
@@ -26,6 +27,7 @@ export const useAPITokenVariable = ({
title: 'API token',
noDisabled,
noChildren: true,
+ operators: string,
});
return {
diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx b/packages/core/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx
index 36d3b9793f..b28bdb9a2d 100644
--- a/packages/core/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx
+++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx
@@ -87,6 +87,8 @@ interface BaseProps {
*/
deprecated?: boolean;
tooltip?: string;
+ /**支持的操作符 */
+ operators?: any[];
}
interface BaseVariableProviderProps {
@@ -133,6 +135,8 @@ const getChildren = (
: isDisabled({ option, collectionField, uiSchema, targetFieldSchema, getCollectionField })),
isLeaf: true,
depth,
+ operators: option?.operators,
+ schema: option?.schema,
};
}
@@ -197,6 +201,7 @@ export const useBaseVariable = ({
returnFields = (fields) => fields,
deprecated,
tooltip,
+ operators = [],
}: BaseProps) => {
const compile = useCompile();
const getFilterOptions = useGetFilterOptions();
@@ -276,6 +281,7 @@ export const useBaseVariable = ({
children: [],
disabled: !!deprecated,
deprecated,
+ operators,
} as Option;
}, [uiSchema?.['x-component']]);
diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx b/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx
index 7ea69f241b..892c0e242c 100644
--- a/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx
+++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx
@@ -46,7 +46,9 @@ const getChildren = (
return {
key: option.name,
value: option.name,
+ name: option.name,
label: compile(option.title),
+ title: compile(option.title),
disabled: disabled,
isLeaf: true,
depth,
@@ -59,7 +61,9 @@ const getChildren = (
return {
key: option.name,
value: option.name,
+ name: option.name,
label: compile(option.title),
+ title: compile(option.title),
disabled: disabled,
isLeaf: true,
field: option,
@@ -77,10 +81,10 @@ export const useContextAssociationFields = ({
contextCollectionName,
collectionField,
}: {
- schema: any;
+ schema?: any;
maxDepth?: number;
contextCollectionName: string;
- collectionField: CollectionFieldOptions_deprecated;
+ collectionField?: CollectionFieldOptions_deprecated;
}) => {
const { t } = useTranslation();
const compile = useCompile();
@@ -101,10 +105,17 @@ export const useContextAssociationFields = ({
const children =
getChildren(
getFilterOptions(collectionName).filter((v) => {
- const isAssociationField = ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(
- v.type,
- );
- return isAssociationField;
+ if (collectionField) {
+ const isAssociationField = [
+ 'hasOne',
+ 'hasMany',
+ 'belongsTo',
+ 'belongsToMany',
+ 'belongsToArray',
+ ].includes(v.type);
+ return isAssociationField;
+ }
+ return true;
}),
{
schema,
diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts
index 933938f120..f2e1f0f9e1 100644
--- a/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts
+++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
import { useOperators } from '../../../block-provider/CollectOperators';
import { useDatePickerContext } from '../../../schema-component/antd/date-picker/DatePicker';
import { getDateRanges } from '../../../schema-component/antd/date-picker/util';
-
+import { datetime } from '../../../collection-manager/interfaces/properties/operators';
interface Props {
operator?: {
value: string;
@@ -45,132 +45,155 @@ export const useDateVariable = ({ operator, schema, noDisabled }: Props) => {
value: 'now',
label: t('Current time'),
disabled: noDisabled ? false : schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
+ operators: datetime,
+ schema: {},
},
{
key: 'yesterday',
value: 'yesterday',
label: t('Yesterday'),
disabled,
+ operators: datetime,
},
{
key: 'today',
value: 'today',
label: t('Today'),
disabled,
+ operators: datetime,
},
{
key: 'tomorrow',
value: 'tomorrow',
label: t('Tomorrow'),
disabled,
+ operators: datetime,
},
{
key: 'lastIsoWeek',
value: 'lastIsoWeek',
label: t('Last week'),
disabled,
+ operators: datetime,
},
{
key: 'thisIsoWeek',
value: 'thisIsoWeek',
label: t('This week'),
disabled,
+ operators: datetime,
},
{
key: 'nextIsoWeek',
value: 'nextIsoWeek',
label: t('Next week'),
disabled,
+ operators: datetime,
},
{
key: 'lastMonth',
value: 'lastMonth',
label: t('Last month'),
disabled,
+ operators: datetime,
},
{
key: 'thisMonth',
value: 'thisMonth',
label: t('This month'),
disabled,
+ operators: datetime,
},
{
key: 'nextMonth',
value: 'nextMonth',
label: t('Next month'),
disabled,
+ operators: datetime,
},
{
key: 'lastQuarter',
value: 'lastQuarter',
label: t('Last quarter'),
disabled,
+ operators: datetime,
},
{
key: 'thisQuarter',
value: 'thisQuarter',
label: t('This quarter'),
disabled,
+ operators: datetime,
},
{
key: 'nextQuarter',
value: 'nextQuarter',
label: t('Next quarter'),
disabled,
+ operators: datetime,
},
{
key: 'lastYear',
value: 'lastYear',
label: t('Last year'),
disabled,
+ operators: datetime,
},
{
key: 'thisYear',
value: 'thisYear',
label: t('This year'),
disabled,
+ operators: datetime,
},
{
key: 'nextYear',
value: 'nextYear',
label: t('Next year'),
disabled,
+ operators: datetime,
},
{
key: 'last7Days',
value: 'last7Days',
label: t('Last 7 days'),
disabled,
+ operators: datetime,
},
{
key: 'next7Days',
value: 'next7Days',
label: t('Next 7 days'),
disabled,
+ operators: datetime,
},
{
key: 'last30Days',
value: 'last30Days',
label: t('Last 30 days'),
disabled,
+ operators: datetime,
},
{
key: 'next30Days',
value: 'next30Days',
label: t('Next 30 days'),
disabled,
+ operators: datetime,
},
{
key: 'last90Days',
value: 'last90Days',
label: t('Last 90 days'),
disabled,
+ operators: datetime,
},
{
key: 'next90Days',
value: 'next90Days',
label: t('Next 90 days'),
disabled,
+ operators: datetime,
},
];
@@ -222,132 +245,154 @@ export const useDatetimeVariable = ({ operator, schema, noDisabled, targetFieldS
value: 'now',
label: t('Current time'),
disabled: noDisabled ? false : schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
+ operators: datetime,
},
{
key: 'yesterday',
value: 'yesterday',
label: t('Yesterday'),
disabled,
+ operators: datetime,
},
{
key: 'today',
value: 'today',
label: t('Today'),
disabled,
+ operators: datetime,
},
{
key: 'tomorrow',
value: 'tomorrow',
label: t('Tomorrow'),
disabled,
+ operators: datetime,
},
{
key: 'lastIsoWeek',
value: 'lastIsoWeek',
label: t('Last week'),
disabled,
+ operators: datetime,
},
{
key: 'thisIsoWeek',
value: 'thisIsoWeek',
label: t('This week'),
disabled,
+ operators: datetime,
},
{
key: 'nextIsoWeek',
value: 'nextIsoWeek',
label: t('Next week'),
disabled,
+ operators: datetime,
},
{
key: 'lastMonth',
value: 'lastMonth',
label: t('Last month'),
disabled,
+ operators: datetime,
},
{
key: 'thisMonth',
value: 'thisMonth',
label: t('This month'),
disabled,
+ operators: datetime,
},
{
key: 'nextMonth',
value: 'nextMonth',
label: t('Next month'),
disabled,
+ operators: datetime,
},
{
key: 'lastQuarter',
value: 'lastQuarter',
label: t('Last quarter'),
disabled,
+ operators: datetime,
},
{
key: 'thisQuarter',
value: 'thisQuarter',
label: t('This quarter'),
disabled,
+ operators: datetime,
},
{
key: 'nextQuarter',
value: 'nextQuarter',
label: t('Next quarter'),
disabled,
+ operators: datetime,
},
{
key: 'lastYear',
value: 'lastYear',
label: t('Last year'),
disabled,
+ operators: datetime,
},
{
key: 'thisYear',
value: 'thisYear',
label: t('This year'),
disabled,
+ operators: datetime,
},
{
key: 'nextYear',
value: 'nextYear',
label: t('Next year'),
disabled,
+ operators: datetime,
},
{
key: 'last7Days',
value: 'last7Days',
label: t('Last 7 days'),
disabled,
+ operators: datetime,
},
{
key: 'next7Days',
value: 'next7Days',
label: t('Next 7 days'),
disabled,
+ operators: datetime,
},
{
key: 'last30Days',
value: 'last30Days',
label: t('Last 30 days'),
disabled,
+ operators: datetime,
},
{
key: 'next30Days',
value: 'next30Days',
label: t('Next 30 days'),
disabled,
+ operators: datetime,
},
{
key: 'last90Days',
value: 'last90Days',
label: t('Last 90 days'),
disabled,
+ operators: datetime,
},
{
key: 'next90Days',
value: 'next90Days',
label: t('Next 90 days'),
disabled,
+ operators: datetime,
},
];
diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts
index f99c6b7644..ddf2f76f1f 100644
--- a/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts
+++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts
@@ -10,6 +10,7 @@
import { Form } from '@formily/core';
import { Schema } from '@formily/json-schema';
import { useTranslation } from 'react-i18next';
+import { useBlockContext } from '../../../block-provider';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
import { useDataBlockRequestData, useDataSource } from '../../../data-source';
@@ -62,14 +63,6 @@ export const useFormVariable = ({ collectionName, collectionField, schema, noDis
return result;
};
-const useCurrentFormData = () => {
- const data = useDataBlockRequestData();
- if (data?.data?.length > 1) {
- return;
- }
- return data?.data?.[0] || data?.data;
-};
-
/**
* 变量:`当前表单` 相关的 hook
* @param param0
@@ -78,14 +71,14 @@ const useCurrentFormData = () => {
export const useCurrentFormContext = ({ form: _form }: Pick = {}) => {
const { form } = useFormBlockContext();
const { isVariableParsedInOtherContext } = useFlag();
-
+ const { name } = useBlockContext?.() || {};
const formInstance = _form || form;
-
return {
/** 变量值 */
currentFormCtx: formInstance?.values,
/** 用来判断是否可以显示`当前表单`变量 */
- shouldDisplayCurrentForm: formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext,
+ shouldDisplayCurrentForm:
+ name === 'form' && formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext,
};
};
diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useRecordVariable.tsx b/packages/core/client/src/schema-settings/VariableInput/hooks/useRecordVariable.tsx
index 33d123fa51..fe00105d3b 100644
--- a/packages/core/client/src/schema-settings/VariableInput/hooks/useRecordVariable.tsx
+++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useRecordVariable.tsx
@@ -90,7 +90,10 @@ export const useCurrentRecordContext = () => {
/** 变量值 */
currentRecordCtx: ctx?.recordData || formRecord?.data || recordData,
/** 用于判断是否需要显示配置项 */
- shouldDisplayCurrentRecord: !_.isEmpty(_.omit(recordData, ['__collectionName', '__parent'])) || !!formRecord?.data,
+ shouldDisplayCurrentRecord:
+ !_.isEmpty(_.omit(recordData, ['__collectionName', '__parent'])) ||
+ !!formRecord?.data ||
+ blockType === 'taleColumn',
/** 当前记录对应的 collection name */
collectionName: realCollectionName,
/** 块类型 */
diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useRoleVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useRoleVariable.ts
index 2b4bcec761..eed541fe37 100644
--- a/packages/core/client/src/schema-settings/VariableInput/hooks/useRoleVariable.ts
+++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useRoleVariable.ts
@@ -13,6 +13,9 @@ import { useAPIClient } from '../../../api-client';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
import { CollectionFieldOptions } from '../../../data-source/collection/Collection';
import { useBaseVariable } from './useBaseVariable';
+import { string } from '../../../collection-manager/interfaces/properties/operators';
+import { useCurrentUserContext } from '../../../user/CurrentUserProvider';
+import { useCompile } from '../../../schema-component';
/**
* @deprecated
@@ -47,6 +50,7 @@ export const useRoleVariable = ({
noDisabled,
targetFieldSchema,
noChildren: true,
+ operators: string,
});
return result;
@@ -73,6 +77,9 @@ export const useCurrentRoleVariable = ({
} = {}) => {
const { t } = useTranslation();
const apiClient = useAPIClient();
+ const compile = useCompile();
+ const { data } = useCurrentUserContext() || {};
+ const roles = (data?.data?.roles || []).map(({ name, title }) => ({ name, title: compile(title) }));
const currentRoleSettings = useBaseVariable({
collectionField,
uiSchema,
@@ -83,12 +90,13 @@ export const useCurrentRoleVariable = ({
noDisabled,
targetFieldSchema,
noChildren: true,
+ operators: string,
});
return {
/** 变量配置项 */
currentRoleSettings,
/** 变量的值 */
- currentRoleCtx: apiClient.auth?.role,
+ currentRoleCtx: apiClient.auth?.role === '__union__' ? roles.map((v) => v.name) : apiClient.auth?.role,
};
};
diff --git a/packages/core/client/src/schema-settings/__e2e__/linkageRules.test.ts b/packages/core/client/src/schema-settings/__e2e__/linkageRules.test.ts
index f52d12eba1..ccf229c420 100644
--- a/packages/core/client/src/schema-settings/__e2e__/linkageRules.test.ts
+++ b/packages/core/client/src/schema-settings/__e2e__/linkageRules.test.ts
@@ -10,6 +10,7 @@
import { expect, test } from '@nocobase/test/e2e';
import {
formFieldDependsOnSubtableFieldsWithLinkageRules,
+ whenARequiredFieldIsSetToHideRetainValueItShouldBeAbleToSubmitTheFormNormally,
whenClearingARelationshipFieldTheValueOfTheAssociatedFieldShouldBeCleared,
whenSetToHideRetainedValueItShouldNotImpactTheFieldSDefaultValueVariables,
} from './template';
@@ -83,6 +84,28 @@ test.describe('linkage rules', () => {
).toBeVisible();
});
+ test('When a required field is set to "Hide (retain value)", it should be able to submit the form normally', async ({
+ mockPage,
+ page,
+ }) => {
+ await mockPage(whenARequiredFieldIsSetToHideRetainValueItShouldBeAbleToSubmitTheFormNormally).goto();
+
+ // 1. 输入昵称
+ await page
+ .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
+ .getByRole('textbox')
+ .fill('123456');
+
+ // 2. 点击提交
+ await page.getByLabel('action-Action-Submit-submit-').click();
+
+ // 3. 应该能正常提交,不应该被拦截
+ await page.reload();
+ await expect(
+ page.getByLabel('block-item-CardItem-users-table').getByRole('cell', { name: '123456' }),
+ ).toBeVisible();
+ });
+
test('When clearing a relationship field, the value of the associated field should be cleared', async ({
page,
mockPage,
diff --git a/packages/core/client/src/schema-settings/__e2e__/template.ts b/packages/core/client/src/schema-settings/__e2e__/template.ts
index af6bfdde87..d540b5ab2f 100644
--- a/packages/core/client/src/schema-settings/__e2e__/template.ts
+++ b/packages/core/client/src/schema-settings/__e2e__/template.ts
@@ -2215,3 +2215,376 @@ export const accessControlActionWithTable = {
'x-index': 1,
},
};
+export const whenARequiredFieldIsSetToHideRetainValueItShouldBeAbleToSubmitTheFormNormally = {
+ tabSchema: {
+ type: 'void',
+ 'x-component': 'Grid',
+ 'x-initializer': 'page:addBlock',
+ properties: {
+ phefo1qp4yu: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ properties: {
+ e94p5oj6num: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ properties: {
+ fpwdszedqqt: {
+ type: 'void',
+ version: '2.0',
+ 'x-toolbar': 'BlockSchemaToolbar',
+ 'x-settings': 'blockSettings:createForm',
+ 'x-component': 'CardItem',
+ 'x-decorator': 'FormBlockProvider',
+ 'x-acl-action': 'users:create',
+ 'x-app-version': '1.6.21',
+ 'x-decorator-props': {
+ collection: 'users',
+ dataSource: 'main',
+ },
+ 'x-acl-action-props': {
+ skipScopeCheck: true,
+ },
+ _isJSONSchemaObject: true,
+ 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
+ properties: {
+ fy0innr5s9v: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'FormV2',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ 'x-use-component-props': 'useCreateFormBlockProps',
+ properties: {
+ grid: {
+ type: 'void',
+ 'x-uid': '7wwn4d0722h',
+ version: '2.0',
+ 'x-component': 'Grid',
+ 'x-app-version': '1.6.21',
+ 'x-initializer': 'form:configureFields',
+ 'x-linkage-rules': [
+ {
+ actions: [
+ {
+ operator: 'hidden',
+ targetFields: ['username'],
+ },
+ ],
+ condition: {
+ $and: [],
+ },
+ },
+ ],
+ _isJSONSchemaObject: true,
+ properties: {
+ t9ik3qkj7mv: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ properties: {
+ szfeenblh7q: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ properties: {
+ username: {
+ type: 'string',
+ version: '2.0',
+ 'x-toolbar': 'FormItemSchemaToolbar',
+ 'x-settings': 'fieldSettings:FormItem',
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-app-version': '1.6.21',
+ 'x-component-props': {},
+ 'x-collection-field': 'users.username',
+ _isJSONSchemaObject: true,
+ 'x-uid': 'w7j6pbapyz2',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': '4p4e653i4sg',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'ojqn0csar3b',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ ij66m020mn5: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ properties: {
+ '95nih4mt2lf': {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ properties: {
+ nickname: {
+ type: 'string',
+ version: '2.0',
+ 'x-toolbar': 'FormItemSchemaToolbar',
+ 'x-settings': 'fieldSettings:FormItem',
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-app-version': '1.6.21',
+ 'x-component-props': {},
+ 'x-collection-field': 'users.nickname',
+ _isJSONSchemaObject: true,
+ 'x-uid': 'rpmd8swoor7',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'h0z7u62fwn3',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'b3v741bdvtt',
+ 'x-async': false,
+ 'x-index': 2,
+ },
+ },
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ o5pser9a4zy: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'ActionBar',
+ 'x-app-version': '1.6.21',
+ 'x-initializer': 'createForm:configureActions',
+ 'x-component-props': {
+ layout: 'one-column',
+ },
+ _isJSONSchemaObject: true,
+ properties: {
+ g5aat11143b: {
+ type: 'void',
+ title: '{{ t("Submit") }}',
+ version: '2.0',
+ 'x-action': 'submit',
+ 'x-toolbar': 'ActionSchemaToolbar',
+ 'x-settings': 'actionSettings:createSubmit',
+ 'x-component': 'Action',
+ 'x-app-version': '1.6.21',
+ 'x-action-settings': {},
+ 'x-component-props': {
+ type: 'primary',
+ htmlType: 'submit',
+ },
+ _isJSONSchemaObject: true,
+ 'x-use-component-props': 'useCreateActionProps',
+ 'x-uid': 'doarbo27x5f',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'n5j6q3dfzst',
+ 'x-async': false,
+ 'x-index': 2,
+ },
+ },
+ 'x-uid': '1k05k8tv3fp',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': '4bq4fts48ec',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'tgjtas03gh6',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'raas2rrgtgq',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ gqnjk39afuj: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ properties: {
+ fvn883pa5lt: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ properties: {
+ '0wlnuaz623s': {
+ type: 'void',
+ version: '2.0',
+ 'x-toolbar': 'BlockSchemaToolbar',
+ 'x-settings': 'blockSettings:table',
+ 'x-component': 'CardItem',
+ 'x-decorator': 'TableBlockProvider',
+ 'x-acl-action': 'users:list',
+ 'x-app-version': '1.6.21',
+ 'x-filter-targets': [],
+ 'x-decorator-props': {
+ action: 'list',
+ params: {
+ pageSize: 20,
+ },
+ rowKey: 'id',
+ dragSort: false,
+ showIndex: true,
+ collection: 'users',
+ dataSource: 'main',
+ },
+ _isJSONSchemaObject: true,
+ 'x-use-decorator-props': 'useTableBlockDecoratorProps',
+ properties: {
+ actions: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'ActionBar',
+ 'x-app-version': '1.6.21',
+ 'x-initializer': 'table:configureActions',
+ 'x-component-props': {
+ style: {
+ marginBottom: 'var(--nb-spacing)',
+ },
+ },
+ _isJSONSchemaObject: true,
+ 'x-uid': 'q7y8z49vkvs',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ e7dmvrnhhhw: {
+ type: 'array',
+ version: '2.0',
+ 'x-component': 'TableV2',
+ 'x-app-version': '1.6.21',
+ 'x-initializer': 'table:configureColumns',
+ 'x-component-props': {
+ rowKey: 'id',
+ rowSelection: {
+ type: 'checkbox',
+ },
+ },
+ _isJSONSchemaObject: true,
+ 'x-use-component-props': 'useTableBlockProps',
+ properties: {
+ actions: {
+ type: 'void',
+ title: '{{ t("Actions") }}',
+ version: '2.0',
+ 'x-toolbar': 'TableColumnSchemaToolbar',
+ 'x-settings': 'fieldSettings:TableColumn',
+ 'x-component': 'TableV2.Column',
+ 'x-decorator': 'TableV2.Column.ActionBar',
+ 'x-app-version': '1.6.21',
+ 'x-initializer': 'table:configureItemActions',
+ 'x-action-column': 'actions',
+ 'x-toolbar-props': {
+ initializer: 'table:configureItemActions',
+ },
+ _isJSONSchemaObject: true,
+ properties: {
+ wwt3qrk0ro0: {
+ type: 'void',
+ version: '2.0',
+ 'x-component': 'Space',
+ 'x-decorator': 'DndContext',
+ 'x-app-version': '1.6.21',
+ 'x-component-props': {
+ split: '|',
+ },
+ _isJSONSchemaObject: true,
+ 'x-uid': '58ymkonaijm',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': '46zml84998i',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ d1qruxltt1t: {
+ type: 'void',
+ version: '2.0',
+ 'x-toolbar': 'TableColumnSchemaToolbar',
+ 'x-settings': 'fieldSettings:TableColumn',
+ 'x-component': 'TableV2.Column',
+ 'x-decorator': 'TableV2.Column.Decorator',
+ 'x-app-version': '1.6.21',
+ _isJSONSchemaObject: true,
+ properties: {
+ nickname: {
+ version: '2.0',
+ 'x-component': 'CollectionField',
+ 'x-decorator': null,
+ 'x-app-version': '1.6.21',
+ 'x-read-pretty': true,
+ 'x-component-props': {
+ ellipsis: true,
+ },
+ 'x-decorator-props': {
+ labelStyle: {
+ display: 'none',
+ },
+ },
+ 'x-collection-field': 'users.nickname',
+ _isJSONSchemaObject: true,
+ 'x-uid': 'ut8m8l3qhzn',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'c2592dzb9s1',
+ 'x-async': false,
+ 'x-index': 2,
+ },
+ },
+ 'x-uid': 'nntvcjy39cg',
+ 'x-async': false,
+ 'x-index': 2,
+ },
+ },
+ 'x-uid': 'o5gizqch1wr',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': '8awil00a9wo',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'i1dnlv9n2k0',
+ 'x-async': false,
+ 'x-index': 2,
+ },
+ },
+ name: 'ri6dthungp3',
+ 'x-uid': 'b2u59skxpsy',
+ 'x-async': true,
+ 'x-index': 1,
+ },
+};
diff --git a/packages/core/client/src/user/CurrentUserProvider.tsx b/packages/core/client/src/user/CurrentUserProvider.tsx
index e8e8f1969c..8df134eb79 100644
--- a/packages/core/client/src/user/CurrentUserProvider.tsx
+++ b/packages/core/client/src/user/CurrentUserProvider.tsx
@@ -27,7 +27,7 @@ export const useIsLoggedIn = () => {
export const useCurrentRoles = () => {
const { allowAnonymous } = useACLRoleContext();
- const { data } = useCurrentUserContext();
+ const { data } = useCurrentUserContext() || {};
const compile = useCompile();
const options = useMemo(() => {
const roles = (data?.data?.roles || []).map(({ name, title }) => ({ name, title: compile(title) }));
diff --git a/packages/core/client/src/variables/utils/isVariable.tsx b/packages/core/client/src/variables/utils/isVariable.tsx
index 6ec5d9ed69..7ad14a0c75 100644
--- a/packages/core/client/src/variables/utils/isVariable.tsx
+++ b/packages/core/client/src/variables/utils/isVariable.tsx
@@ -8,7 +8,7 @@
*/
import { extractTemplateVariable } from '@nocobase/json-template-parser';
-export const REGEX_OF_VARIABLE = /^\s*\{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}\s*$/g;
+export const REGEX_OF_VARIABLE = /^\s*\{\{\s*([\p{L}0-9_$-.]+?)\s*\}\}\s*$/u;
export const REGEX_OF_VARIABLE_IN_EXPRESSION = /\{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}/g;
export const isVariable = (str: unknown) => {
diff --git a/packages/core/create-nocobase-app/package.json b/packages/core/create-nocobase-app/package.json
index e0e866a138..446b618c59 100755
--- a/packages/core/create-nocobase-app/package.json
+++ b/packages/core/create-nocobase-app/package.json
@@ -1,6 +1,6 @@
{
"name": "create-nocobase-app",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "src/index.js",
"license": "AGPL-3.0",
"dependencies": {
@@ -8,7 +8,8 @@
"axios": "^1.7.0",
"chalk": "^4.1.1",
"commander": "^9.2.0",
- "tar": "6.1.11"
+ "fs-extra": "^11.3.0",
+ "tar": "^7.4.3"
},
"bin": {
"create-nocobase-app": "./bin/index.js"
diff --git a/packages/core/create-nocobase-app/src/cli.js b/packages/core/create-nocobase-app/src/cli.js
index 09fd152ddc..0fd40eb008 100644
--- a/packages/core/create-nocobase-app/src/cli.js
+++ b/packages/core/create-nocobase-app/src/cli.js
@@ -19,6 +19,7 @@ const cli = new Command('create-nocobase');
cli
.arguments('', 'directory of new NocoBase app')
.option('--quickstart', 'quickstart app creation')
+ .option('--skip-dev-dependencies')
.option('-a, --all-db-dialect', 'install all database dialect dependencies')
.option('-d, --db-dialect ', 'database dialect, current support sqlite/mysql/postgres', 'sqlite')
.option('-e, --env ', 'environment variables write into .env file', concat, [])
diff --git a/packages/core/create-nocobase-app/src/generator.js b/packages/core/create-nocobase-app/src/generator.js
index 260cbc308f..fc7ef0179d 100644
--- a/packages/core/create-nocobase-app/src/generator.js
+++ b/packages/core/create-nocobase-app/src/generator.js
@@ -9,7 +9,8 @@
const chalk = require('chalk');
const crypto = require('crypto');
-const { existsSync } = require('fs');
+const { existsSync, promises } = require('fs');
+const fs = require('fs-extra');
const { join, resolve } = require('path');
const { Generator } = require('@umijs/utils');
const { downloadPackageFromNpm, updateJsonFile } = require('./util');
@@ -191,6 +192,13 @@ class AppGenerator extends Generator {
this.checkDbEnv();
+ const skipDevDependencies = this.args.skipDevDependencies;
+ if (skipDevDependencies) {
+ const json = await fs.readJSON(join(this.cwd, 'package.json'), 'utf8');
+ delete json['devDependencies'];
+ await fs.writeJSON(join(this.cwd, 'package.json'), json, { encoding: 'utf8', spaces: 2 });
+ }
+
console.log('');
console.log(chalk.green(`$ cd ${name}`));
console.log(chalk.green(`$ yarn install`));
diff --git a/packages/core/create-nocobase-app/templates/app/package.json.tpl b/packages/core/create-nocobase-app/templates/app/package.json.tpl
index 392cfdbf4b..6245ac2fd6 100644
--- a/packages/core/create-nocobase-app/templates/app/package.json.tpl
+++ b/packages/core/create-nocobase-app/templates/app/package.json.tpl
@@ -29,6 +29,7 @@
"react-router-dom": "6.28.1",
"react-router": "6.28.1",
"antd": "5.24.2",
+ "async": "3.2.6",
"rollup": "4.24.0"
},
"dependencies": {
diff --git a/packages/core/data-source-manager/package.json b/packages/core/data-source-manager/package.json
index b765f7792a..5774d23528 100644
--- a/packages/core/data-source-manager/package.json
+++ b/packages/core/data-source-manager/package.json
@@ -1,16 +1,16 @@
{
"name": "@nocobase/data-source-manager",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
- "@nocobase/actions": "1.7.0-alpha.10",
- "@nocobase/cache": "1.7.0-alpha.10",
- "@nocobase/database": "1.7.0-alpha.10",
- "@nocobase/resourcer": "1.7.0-alpha.10",
- "@nocobase/utils": "1.7.0-alpha.10",
+ "@nocobase/actions": "1.7.0-alpha.11",
+ "@nocobase/cache": "1.7.0-alpha.11",
+ "@nocobase/database": "1.7.0-alpha.11",
+ "@nocobase/resourcer": "1.7.0-alpha.11",
+ "@nocobase/utils": "1.7.0-alpha.11",
"@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1"
},
diff --git a/packages/core/database/package.json b/packages/core/database/package.json
index 10b3a15682..5dd1ec75a0 100644
--- a/packages/core/database/package.json
+++ b/packages/core/database/package.json
@@ -1,13 +1,13 @@
{
"name": "@nocobase/database",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
"dependencies": {
- "@nocobase/logger": "1.7.0-alpha.10",
- "@nocobase/utils": "1.7.0-alpha.10",
+ "@nocobase/logger": "1.7.0-alpha.11",
+ "@nocobase/utils": "1.7.0-alpha.11",
"async-mutex": "^0.3.2",
"chalk": "^4.1.1",
"cron-parser": "4.4.0",
@@ -20,12 +20,12 @@
"graphlib": "^2.1.8",
"lodash": "^4.17.21",
"mathjs": "^10.6.1",
- "nanoid": "^3.3.6",
+ "nanoid": "^3.3.11",
"node-fetch": "^2.6.7",
"node-sql-parser": "^4.18.0",
"qs": "^6.11.2",
"safe-json-stringify": "^1.2.0",
- "semver": "^7.3.7",
+ "semver": "^7.7.1",
"sequelize": "^6.26.0",
"umzug": "^3.1.1",
"uuid": "^9.0.1"
diff --git a/packages/core/database/src/__tests__/fields/string-field.test.ts b/packages/core/database/src/__tests__/fields/string-field.test.ts
index ee4f3e631d..ff095bc862 100644
--- a/packages/core/database/src/__tests__/fields/string-field.test.ts
+++ b/packages/core/database/src/__tests__/fields/string-field.test.ts
@@ -105,4 +105,18 @@ describe('string field', () => {
name2: 'n2111',
});
});
+
+ it('trim', async () => {
+ const collection = db.collection({
+ name: 'tests',
+ fields: [{ type: 'string', name: 'name', trim: true }],
+ });
+ await db.sync();
+ const model = await collection.model.create({
+ name: ' n1\n ',
+ });
+ expect(model.toJSON()).toMatchObject({
+ name: 'n1',
+ });
+ });
});
diff --git a/packages/core/database/src/__tests__/fields/text-field.test.ts b/packages/core/database/src/__tests__/fields/text-field.test.ts
index b9c19e69f1..762269cb7f 100644
--- a/packages/core/database/src/__tests__/fields/text-field.test.ts
+++ b/packages/core/database/src/__tests__/fields/text-field.test.ts
@@ -52,4 +52,18 @@ describe('text field', () => {
});
await Test.sync();
});
+
+ it('trim', async () => {
+ const collection = db.collection({
+ name: 'tests',
+ fields: [{ type: 'text', name: 'name', trim: true }],
+ });
+ await db.sync();
+ const model = await collection.model.create({
+ name: ' n1\n ',
+ });
+ expect(model.toJSON()).toMatchObject({
+ name: 'n1',
+ });
+ });
});
diff --git a/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts
index 78793cc356..2cd0dd6bb7 100644
--- a/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts
+++ b/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts
@@ -159,6 +159,7 @@ describe('has many repository', () => {
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
+ { type: 'belongsTo', name: 'user' },
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
{ type: 'hasMany', name: 'comments' },
{ type: 'string', name: 'status' },
@@ -480,6 +481,51 @@ describe('has many repository', () => {
).not.toBeNull();
});
+ test('destroy by pk and filter with association', async () => {
+ const u1 = await User.repository.create({
+ values: { name: 'u1' },
+ });
+
+ const UserPostRepository = new HasManyRepository(User, 'posts', u1.id);
+
+ const p1 = await UserPostRepository.create({
+ values: {
+ title: 't1',
+ status: 'published',
+ user: u1,
+ },
+ });
+
+ const p2 = await UserPostRepository.create({
+ values: {
+ title: 't2',
+ status: 'draft',
+ user: u1,
+ },
+ });
+
+ await UserPostRepository.destroy({
+ filterByTk: p1.id,
+ filter: {
+ user: {
+ id: u1.id,
+ },
+ },
+ });
+
+ expect(
+ await UserPostRepository.findOne({
+ filterByTk: p1.id,
+ }),
+ ).toBeNull();
+
+ expect(
+ await UserPostRepository.findOne({
+ filterByTk: p2.id,
+ }),
+ ).not.toBeNull();
+ });
+
test('destroy by pk', async () => {
const u1 = await User.repository.create({
values: { name: 'u1' },
diff --git a/packages/core/database/src/belongs-to-array/belongs-to-array-repository.ts b/packages/core/database/src/belongs-to-array/belongs-to-array-repository.ts
index 375e65829e..254baf3aa0 100644
--- a/packages/core/database/src/belongs-to-array/belongs-to-array-repository.ts
+++ b/packages/core/database/src/belongs-to-array/belongs-to-array-repository.ts
@@ -54,19 +54,18 @@ export class BelongsToArrayAssociation {
return this.db.getModel(this.targetName);
}
- generateInclude() {
- if (this.db.sequelize.getDialect() !== 'postgres') {
- throw new Error('Filtering by many to many (array) associations is only supported on postgres');
- }
+ generateInclude(parentAs?: string) {
const targetCollection = this.db.getCollection(this.targetName);
const targetField = targetCollection.getField(this.targetKey);
const sourceCollection = this.db.getCollection(this.source.name);
const foreignField = sourceCollection.getField(this.foreignKey);
const queryInterface = this.db.sequelize.getQueryInterface();
- const left = queryInterface.quoteIdentifiers(`${this.as}.${targetField.columnName()}`);
- const right = queryInterface.quoteIdentifiers(`${this.source.collection.name}.${foreignField.columnName()}`);
+ const asLeft = parentAs ? `${parentAs}->${this.as}` : this.as;
+ const asRight = parentAs || this.source.collection.name;
+ const left = queryInterface.quoteIdentifiers(`${asLeft}.${targetField.columnName()}`);
+ const right = queryInterface.quoteIdentifiers(`${asRight}.${foreignField.columnName()}`);
return {
- on: this.db.sequelize.literal(`${left}=any(${right})`),
+ on: this.db.queryInterface.generateJoinOnForJSONArray(left, right),
};
}
diff --git a/packages/core/database/src/eager-loading/eager-loading-tree.ts b/packages/core/database/src/eager-loading/eager-loading-tree.ts
index d0d9fb2eeb..c450eb816f 100644
--- a/packages/core/database/src/eager-loading/eager-loading-tree.ts
+++ b/packages/core/database/src/eager-loading/eager-loading-tree.ts
@@ -82,6 +82,31 @@ const queryParentSQL = (options: {
SELECT ${q(targetKeyField)} AS ${q(targetKey)}, ${q(foreignKeyField)} AS ${q(foreignKey)} FROM cte`;
};
+const processIncludes = (includes: any[], model: any, parentAs = '') => {
+ includes.forEach((include: { association: string; include?: any[] }, index: number) => {
+ // Process current level
+ const association = model.associations[include.association];
+ if (association?.generateInclude) {
+ includes[index] = {
+ ...include,
+ ...association.generateInclude(parentAs),
+ };
+ }
+
+ // Recursively process nested includes if they exist
+ if (include.include && Array.isArray(include.include) && include.include.length > 0) {
+ // Get the associated model for the next level
+ const nextModel = association?.target;
+ if (!nextModel) {
+ return;
+ }
+ processIncludes(include.include, nextModel, parentAs ? `${parentAs}->${association.as}` : association.as);
+ }
+ });
+
+ return includes;
+};
+
export class EagerLoadingTree {
public root: EagerLoadingNode;
db: Database;
@@ -252,16 +277,6 @@ export class EagerLoadingTree {
throw new Error(`Model ${node.model.name} does not have primary key`);
}
- includeForFilter.forEach((include: { association: string }, index: number) => {
- const association = node.model.associations[include.association];
- if (association?.associationType == 'BelongsToArray') {
- includeForFilter[index] = {
- ...include,
- ...association.generateInclude(),
- };
- }
- });
-
// find all ids
const ids = (
await node.model.findAll({
@@ -270,7 +285,7 @@ export class EagerLoadingTree {
attributes: [primaryKeyField],
group: `${node.model.name}.${primaryKeyField}`,
transaction,
- include: includeForFilter,
+ include: processIncludes(includeForFilter, node.model),
} as any)
).map((row) => {
return { row, pk: row[primaryKeyField] };
diff --git a/packages/core/database/src/fields/datetime-no-tz-field.ts b/packages/core/database/src/fields/datetime-no-tz-field.ts
index 695ace4813..676ac778a2 100644
--- a/packages/core/database/src/fields/datetime-no-tz-field.ts
+++ b/packages/core/database/src/fields/datetime-no-tz-field.ts
@@ -29,26 +29,24 @@ export class DatetimeNoTzField extends Field {
return DatetimeNoTzTypeMySQL;
}
- return DataTypes.STRING;
+ return DataTypes.DATE;
}
- init() {
+ beforeSave = async (instance, options) => {
const { name, defaultToCurrentTime, onUpdateToCurrentTime } = this.options;
- this.beforeSave = async (instance, options) => {
- const value = instance.get(name);
+ const value = instance.get(name);
- if (!value && instance.isNewRecord && defaultToCurrentTime) {
- instance.set(name, new Date());
- return;
- }
+ if (!value && instance.isNewRecord && defaultToCurrentTime) {
+ instance.set(name, new Date());
+ return;
+ }
- if (onUpdateToCurrentTime) {
- instance.set(name, new Date());
- return;
- }
- };
- }
+ if (onUpdateToCurrentTime) {
+ instance.set(name, new Date());
+ return;
+ }
+ };
additionalSequelizeOptions(): {} {
const { name } = this.options;
@@ -57,17 +55,14 @@ export class DatetimeNoTzField extends Field {
const timezone = this.database.options.rawTimezone || '+00:00';
const isPg = this.database.inDialect('postgres');
+ const isMySQLCompatibleDialect = this.database.isMySQLCompatibleDialect();
return {
get() {
const val = this.getDataValue(name);
if (val instanceof Date) {
- if (isPg) {
- return moment(val).format('YYYY-MM-DD HH:mm:ss');
- }
- // format to YYYY-MM-DD HH:mm:ss
- const momentVal = moment(val).utcOffset(timezone);
+ const momentVal = moment(val);
return momentVal.format('YYYY-MM-DD HH:mm:ss');
}
@@ -75,18 +70,24 @@ export class DatetimeNoTzField extends Field {
},
set(val) {
- if (typeof val === 'string' && isIso8601(val)) {
- const momentVal = moment(val).utcOffset(timezone);
- val = momentVal.format('YYYY-MM-DD HH:mm:ss');
+ if (val == null) {
+ return this.setDataValue(name, null);
}
- if (val && val instanceof Date) {
- // format to YYYY-MM-DD HH:mm:ss
- const momentVal = moment(val).utcOffset(timezone);
- val = momentVal.format('YYYY-MM-DD HH:mm:ss');
+ const dateOffset = new Date().getTimezoneOffset();
+ const momentVal = moment(val);
+ if ((typeof val === 'string' && isIso8601(val)) || val instanceof Date) {
+ momentVal.utcOffset(timezone);
+ momentVal.utcOffset(-dateOffset, true);
}
- return this.setDataValue(name, val);
+ if (isMySQLCompatibleDialect) {
+ momentVal.millisecond(0);
+ }
+
+ const date = momentVal.toDate();
+
+ return this.setDataValue(name, date);
},
};
}
diff --git a/packages/core/database/src/fields/string-field.ts b/packages/core/database/src/fields/string-field.ts
index 50431f0565..627768beeb 100644
--- a/packages/core/database/src/fields/string-field.ts
+++ b/packages/core/database/src/fields/string-field.ts
@@ -8,7 +8,7 @@
*/
import { DataTypes } from 'sequelize';
-import { BaseColumnFieldOptions, Field } from './field';
+import { BaseColumnFieldOptions, Field, FieldContext } from './field';
export class StringField extends Field {
get dataType() {
@@ -18,9 +18,20 @@ export class StringField extends Field {
return DataTypes.STRING;
}
+
+ additionalSequelizeOptions() {
+ const { name, trim } = this.options;
+
+ return {
+ set(value) {
+ this.setDataValue(name, trim ? value?.trim() : value);
+ },
+ };
+ }
}
export interface StringFieldOptions extends BaseColumnFieldOptions {
type: 'string';
length?: number;
+ trim?: boolean;
}
diff --git a/packages/core/database/src/fields/text-field.ts b/packages/core/database/src/fields/text-field.ts
index 74ca88da9b..39f1ef4670 100644
--- a/packages/core/database/src/fields/text-field.ts
+++ b/packages/core/database/src/fields/text-field.ts
@@ -23,9 +23,20 @@ export class TextField extends Field {
this.options.defaultValue = null;
}
}
+
+ additionalSequelizeOptions() {
+ const { name, trim } = this.options;
+
+ return {
+ set(value) {
+ this.setDataValue(name, trim ? value?.trim() : value);
+ },
+ };
+ }
}
export interface TextFieldOptions extends BaseColumnFieldOptions {
type: 'text';
length?: 'tiny' | 'medium' | 'long';
+ trim?: boolean;
}
diff --git a/packages/core/database/src/interfaces/index.ts b/packages/core/database/src/interfaces/index.ts
index 038ada52cc..aebd38d8d1 100644
--- a/packages/core/database/src/interfaces/index.ts
+++ b/packages/core/database/src/interfaces/index.ts
@@ -15,3 +15,4 @@ export * from './datetime-interface';
export * from './datetime-no-tz-interface';
export * from './boolean-interface';
export * from './date-interface';
+export * from './time-interface';
diff --git a/packages/core/database/src/interfaces/time-interface.ts b/packages/core/database/src/interfaces/time-interface.ts
new file mode 100644
index 0000000000..6b14579b30
--- /dev/null
+++ b/packages/core/database/src/interfaces/time-interface.ts
@@ -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.
+ */
+
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc';
+import { BaseInterface } from './base-interface';
+dayjs.extend(utc);
+export class TimeInterface extends BaseInterface {
+ toValue(value: any, ctx?: any) {
+ if (this.validate(value)) {
+ const result = dayjs.utc(value).format('HH:mm:ss');
+ return result;
+ }
+ return value;
+ }
+
+ validate(value) {
+ const result = dayjs(value).isValid();
+ return result;
+ }
+}
diff --git a/packages/core/database/src/interfaces/utils.ts b/packages/core/database/src/interfaces/utils.ts
index 170687f6e5..cc10790ad0 100644
--- a/packages/core/database/src/interfaces/utils.ts
+++ b/packages/core/database/src/interfaces/utils.ts
@@ -16,6 +16,7 @@ import {
MultipleSelectInterface,
PercentInterface,
SelectInterface,
+ TimeInterface,
} from './index';
import { ManyToOneInterface } from './many-to-one-interface';
import { ManyToManyInterface } from './many-to-many-interface';
@@ -50,6 +51,7 @@ const interfaces = {
o2m: OneToManyInterface,
m2o: ManyToOneInterface,
m2m: ManyToManyInterface,
+ time: TimeInterface,
};
export function registerInterfaces(db: Database) {
diff --git a/packages/core/database/src/query-interface/mysql-query-interface.ts b/packages/core/database/src/query-interface/mysql-query-interface.ts
index fff3afa528..62c2c7febd 100644
--- a/packages/core/database/src/query-interface/mysql-query-interface.ts
+++ b/packages/core/database/src/query-interface/mysql-query-interface.ts
@@ -141,4 +141,8 @@ export default class MysqlQueryInterface extends QueryInterface {
await this.db.sequelize.query(sql, { transaction });
}
}
+
+ public generateJoinOnForJSONArray(left: string, right: string) {
+ return this.db.sequelize.literal(`JSON_CONTAINS(${right}, JSON_ARRAY(${left}))`);
+ }
}
diff --git a/packages/core/database/src/query-interface/postgres-query-interface.ts b/packages/core/database/src/query-interface/postgres-query-interface.ts
index 2d251ddf51..0b38c7c098 100644
--- a/packages/core/database/src/query-interface/postgres-query-interface.ts
+++ b/packages/core/database/src/query-interface/postgres-query-interface.ts
@@ -232,4 +232,8 @@ $BODY$
return res[0]['show_create_table'];
}
+
+ public generateJoinOnForJSONArray(left: string, right: string) {
+ return this.db.sequelize.literal(`${left}=any(${right})`);
+ }
}
diff --git a/packages/core/database/src/query-interface/query-interface.ts b/packages/core/database/src/query-interface/query-interface.ts
index c7d484ccb7..9d8ffe349a 100644
--- a/packages/core/database/src/query-interface/query-interface.ts
+++ b/packages/core/database/src/query-interface/query-interface.ts
@@ -83,4 +83,9 @@ export default abstract class QueryInterface {
// @ts-ignore
return this.db.sequelize.getQueryInterface().queryGenerator.quoteIdentifier(identifier);
}
+
+ public generateJoinOnForJSONArray(left: string, right: string) {
+ const dialect = this.db.sequelize.getDialect();
+ throw new Error(`Filtering by many to many (array) associations is not supported on ${dialect}`);
+ }
}
diff --git a/packages/core/database/src/query-interface/sqlite-query-interface.ts b/packages/core/database/src/query-interface/sqlite-query-interface.ts
index 2f6b93c877..ec77652c56 100644
--- a/packages/core/database/src/query-interface/sqlite-query-interface.ts
+++ b/packages/core/database/src/query-interface/sqlite-query-interface.ts
@@ -146,4 +146,8 @@ export default class SqliteQueryInterface extends QueryInterface {
WHERE name = '${tableName}';`;
await this.db.sequelize.query(sql, { transaction });
}
+
+ public generateJoinOnForJSONArray(left: string, right: string) {
+ return this.db.sequelize.literal(`${left} in (SELECT value from json_each(${right}))`);
+ }
}
diff --git a/packages/core/database/src/relation-repository/hasmany-repository.ts b/packages/core/database/src/relation-repository/hasmany-repository.ts
index f831166d67..4720a6fa59 100644
--- a/packages/core/database/src/relation-repository/hasmany-repository.ts
+++ b/packages/core/database/src/relation-repository/hasmany-repository.ts
@@ -72,7 +72,13 @@ export class HasManyRepository extends MultipleRelationRepository {
const filterResult = this.parseFilter(options['filter'], options);
if (filterResult.include && filterResult.include.length > 0) {
- return await this.destroyByFilter(options['filter'], transaction);
+ return await this.destroyByFilter(
+ {
+ filter: options['filter'],
+ filterByTk: options['filterByTk'],
+ },
+ transaction,
+ );
}
where.push(filterResult.where);
diff --git a/packages/core/database/src/relation-repository/multiple-relation-repository.ts b/packages/core/database/src/relation-repository/multiple-relation-repository.ts
index af3ed91525..9a649fa97c 100644
--- a/packages/core/database/src/relation-repository/multiple-relation-repository.ts
+++ b/packages/core/database/src/relation-repository/multiple-relation-repository.ts
@@ -179,9 +179,15 @@ export abstract class MultipleRelationRepository extends RelationRepository {
return false;
}
- protected async destroyByFilter(filter: Filter, transaction?: Transaction) {
+ protected async destroyByFilter(
+ options: {
+ filter?: Filter;
+ filterByTk?: TargetKey | TargetKey[];
+ },
+ transaction?: Transaction,
+ ) {
const instances = await this.find({
- filter: filter,
+ ...options,
transaction,
});
diff --git a/packages/core/devtools/package.json b/packages/core/devtools/package.json
index c1e97aac68..f6ed8fe856 100644
--- a/packages/core/devtools/package.json
+++ b/packages/core/devtools/package.json
@@ -1,13 +1,13 @@
{
"name": "@nocobase/devtools",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"description": "",
"license": "AGPL-3.0",
"main": "./src/index.js",
"dependencies": {
- "@nocobase/build": "1.7.0-alpha.10",
- "@nocobase/client": "1.7.0-alpha.10",
- "@nocobase/test": "1.7.0-alpha.10",
+ "@nocobase/build": "1.7.0-alpha.11",
+ "@nocobase/client": "1.7.0-alpha.11",
+ "@nocobase/test": "1.7.0-alpha.11",
"@types/koa": "^2.15.0",
"@types/koa-bodyparser": "^4.3.4",
"@types/lodash": "^4.14.177",
@@ -35,7 +35,7 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"rimraf": "^3.0.0",
- "serve": "^13.0.2",
+ "serve": "^14.2.4",
"ts-loader": "^7.0.4",
"ts-node": "9.1.1",
"ts-node-dev": "1.1.8",
diff --git a/packages/core/evaluators/package.json b/packages/core/evaluators/package.json
index 40ab5ae68c..88667c5526 100644
--- a/packages/core/evaluators/package.json
+++ b/packages/core/evaluators/package.json
@@ -1,13 +1,13 @@
{
"name": "@nocobase/evaluators",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
"dependencies": {
"@formulajs/formulajs": "4.4.9",
- "@nocobase/utils": "1.7.0-alpha.10",
+ "@nocobase/utils": "1.7.0-alpha.11",
"mathjs": "^10.6.0"
},
"repository": {
diff --git a/packages/core/lock-manager/package.json b/packages/core/lock-manager/package.json
index 46ffbb6df8..a75559a541 100644
--- a/packages/core/lock-manager/package.json
+++ b/packages/core/lock-manager/package.json
@@ -1,10 +1,10 @@
{
"name": "@nocobase/lock-manager",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "lib/index.js",
"license": "AGPL-3.0",
"devDependencies": {
- "@nocobase/utils": "1.7.0-alpha.10",
+ "@nocobase/utils": "1.7.0-alpha.11",
"async-mutex": "^0.5.0"
}
}
diff --git a/packages/core/logger/package.json b/packages/core/logger/package.json
index 9bc975d571..dfbdd4c74f 100644
--- a/packages/core/logger/package.json
+++ b/packages/core/logger/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/logger",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"description": "nocobase logging library",
"license": "AGPL-3.0",
"main": "./lib/index.js",
diff --git a/packages/core/resourcer/package.json b/packages/core/resourcer/package.json
index cd0cb4f2a5..d7ec61e634 100644
--- a/packages/core/resourcer/package.json
+++ b/packages/core/resourcer/package.json
@@ -1,12 +1,12 @@
{
"name": "@nocobase/resourcer",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
"dependencies": {
- "@nocobase/utils": "1.7.0-alpha.10",
+ "@nocobase/utils": "1.7.0-alpha.11",
"deepmerge": "^4.2.2",
"koa-compose": "^4.1.0",
"lodash": "^4.17.21",
diff --git a/packages/core/resourcer/src/__tests__/koa.test.ts b/packages/core/resourcer/src/__tests__/koa.test.ts
index b3eb0a1b9f..776fc7ed73 100644
--- a/packages/core/resourcer/src/__tests__/koa.test.ts
+++ b/packages/core/resourcer/src/__tests__/koa.test.ts
@@ -215,7 +215,7 @@ describe('koa middleware', () => {
sort: '-id',
});
expect(response.body).toMatchObject({
- sort: ['-id'],
+ sort: '-id',
filter: {
$and: [
{ col1: 'val1', col2: 'val2' },
diff --git a/packages/core/sdk/package.json b/packages/core/sdk/package.json
index 08768cddae..d58e197f72 100644
--- a/packages/core/sdk/package.json
+++ b/packages/core/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/sdk",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",
diff --git a/packages/core/sdk/src/APIClient.ts b/packages/core/sdk/src/APIClient.ts
index 9cb8bc922e..2d360599d7 100644
--- a/packages/core/sdk/src/APIClient.ts
+++ b/packages/core/sdk/src/APIClient.ts
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, RawAxiosRequestHeaders } from 'axios';
+import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, RawAxiosRequestHeaders } from 'axios';
import qs from 'qs';
export interface ActionParams {
diff --git a/packages/core/server/package.json b/packages/core/server/package.json
index 7b99de737b..f456e366c5 100644
--- a/packages/core/server/package.json
+++ b/packages/core/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/server",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
@@ -11,19 +11,19 @@
"@koa/cors": "^5.0.0",
"@koa/multer": "^3.0.2",
"@koa/router": "^9.4.0",
- "@nocobase/acl": "1.7.0-alpha.10",
- "@nocobase/actions": "1.7.0-alpha.10",
- "@nocobase/auth": "1.7.0-alpha.10",
- "@nocobase/cache": "1.7.0-alpha.10",
- "@nocobase/data-source-manager": "1.7.0-alpha.10",
- "@nocobase/database": "1.7.0-alpha.10",
- "@nocobase/evaluators": "1.7.0-alpha.10",
- "@nocobase/lock-manager": "1.7.0-alpha.10",
- "@nocobase/logger": "1.7.0-alpha.10",
- "@nocobase/resourcer": "1.7.0-alpha.10",
- "@nocobase/sdk": "1.7.0-alpha.10",
- "@nocobase/telemetry": "1.7.0-alpha.10",
- "@nocobase/utils": "1.7.0-alpha.10",
+ "@nocobase/acl": "1.7.0-alpha.11",
+ "@nocobase/actions": "1.7.0-alpha.11",
+ "@nocobase/auth": "1.7.0-alpha.11",
+ "@nocobase/cache": "1.7.0-alpha.11",
+ "@nocobase/data-source-manager": "1.7.0-alpha.11",
+ "@nocobase/database": "1.7.0-alpha.11",
+ "@nocobase/evaluators": "1.7.0-alpha.11",
+ "@nocobase/lock-manager": "1.7.0-alpha.11",
+ "@nocobase/logger": "1.7.0-alpha.11",
+ "@nocobase/resourcer": "1.7.0-alpha.11",
+ "@nocobase/sdk": "1.7.0-alpha.11",
+ "@nocobase/telemetry": "1.7.0-alpha.11",
+ "@nocobase/utils": "1.7.0-alpha.11",
"@types/decompress": "4.2.7",
"@types/ini": "^1.3.31",
"@types/koa-send": "^4.1.3",
@@ -32,6 +32,7 @@
"axios": "^1.7.0",
"chalk": "^4.1.1",
"commander": "^9.2.0",
+ "compression": "^1.8.0",
"cron": "^2.4.4",
"cronstrue": "^2.11.0",
"dayjs": "^1.11.8",
@@ -46,9 +47,9 @@
"koa-static": "^5.0.0",
"lodash": "^4.17.21",
"multer": "^1.4.2",
- "nanoid": "3.3.4",
- "semver": "^7.3.7",
- "serve-handler": "^6.1.5",
+ "nanoid": "^3.3.11",
+ "semver": "^7.7.1",
+ "serve-handler": "^6.1.6",
"ws": "^8.13.0",
"xpipe": "^1.0.5"
},
diff --git a/packages/core/server/src/aes-encryptor.tsx b/packages/core/server/src/aes-encryptor.ts
similarity index 100%
rename from packages/core/server/src/aes-encryptor.tsx
rename to packages/core/server/src/aes-encryptor.ts
diff --git a/packages/core/telemetry/package.json b/packages/core/telemetry/package.json
index 1ae9d1b102..c70ab94c78 100644
--- a/packages/core/telemetry/package.json
+++ b/packages/core/telemetry/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/telemetry",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"description": "nocobase telemetry library",
"license": "AGPL-3.0",
"main": "./lib/index.js",
@@ -11,7 +11,7 @@
"directory": "packages/telemetry"
},
"dependencies": {
- "@nocobase/utils": "1.7.0-alpha.10",
+ "@nocobase/utils": "1.7.0-alpha.11",
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/instrumentation": "^0.46.0",
"@opentelemetry/resources": "^1.19.0",
diff --git a/packages/core/test/package.json b/packages/core/test/package.json
index 735c374e8f..ccd4899ec7 100644
--- a/packages/core/test/package.json
+++ b/packages/core/test/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/test",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "lib/index.js",
"module": "./src/index.ts",
"types": "./lib/index.d.ts",
@@ -51,7 +51,7 @@
},
"dependencies": {
"@faker-js/faker": "8.1.0",
- "@nocobase/server": "1.7.0-alpha.10",
+ "@nocobase/server": "1.7.0-alpha.11",
"@playwright/test": "^1.45.3",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.0.0",
diff --git a/packages/core/test/src/e2e/e2eUtils.ts b/packages/core/test/src/e2e/e2eUtils.ts
index 05372aeee2..39dd40af23 100644
--- a/packages/core/test/src/e2e/e2eUtils.ts
+++ b/packages/core/test/src/e2e/e2eUtils.ts
@@ -31,6 +31,21 @@ function getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) {
};
}
+function getPageMenuSchemaWithTabSchema({ tabSchema }) {
+ if (!tabSchema) {
+ return null;
+ }
+
+ return {
+ type: 'void',
+ 'x-component': 'Page',
+ properties: {
+ [tabSchema.name]: tabSchema,
+ },
+ 'x-uid': uid(),
+ };
+}
+
export * from '@playwright/test';
export { defineConfig };
@@ -193,10 +208,17 @@ export interface PageConfig {
*/
collections?: CollectionSetting[];
/**
+ * @deprecate 在菜单被重构之后,没有办法直接复制完整的页面 Schema 了。所以这个选项不推荐使用了。
+ * 推荐使用 tabSchema,复制一个页面 tab 的 Schema 传给 tabSchema。
+ *
* 页面整体的 Schema
* @default undefined
*/
pageSchema?: any;
+ /**
+ * 页面 Tab 的 Schema。当 pageSchema 和 tabSchema 都存在时,最终显示的会是 tabSchema 的内容
+ */
+ tabSchema?: any;
/** 如果为 true 则表示不会更改 PageSchema 的 uid */
keepUid?: boolean;
/** 在 URL 中的 uid,例如:/admin/0ig6xhe03u2 */
@@ -217,6 +239,7 @@ interface CreatePageOptions {
url?: PageConfig['url'];
name?: string;
pageSchema?: any;
+ tabSchema?: any;
/** 如果为 true 则表示不会更改 PageSchema 的 uid */
keepUid?: boolean;
/** 在 URL 中的 uid,例如:/admin/0ig6xhe03u2 */
@@ -367,6 +390,7 @@ export class NocoPage {
type: this.options?.type,
name: this.options?.name,
pageSchema: this.options?.pageSchema,
+ tabSchema: this.options?.tabSchema,
url: this.options?.url,
keepUid: this.options?.keepUid,
pageUid: this.options?.pageUid,
@@ -737,13 +761,16 @@ const updateUidOfPageSchema = (uiSchema: any) => {
* 在 NocoBase 中创建一个页面
*/
const createPage = async (options?: CreatePageOptions) => {
- const { type = 'page', url, name, pageSchema, keepUid, pageUid: pageUidFromOptions } = options || {};
+ const { type = 'page', url, name, pageSchema, tabSchema, keepUid, pageUid: pageUidFromOptions } = options || {};
const api = await request.newContext({
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
});
+
+ const schema = getPageMenuSchemaWithTabSchema({ tabSchema }) || pageSchema;
+
const state = await api.storageState();
const headers = getHeaders(state);
- const newPageSchema = keepUid ? pageSchema : updateUidOfPageSchema(pageSchema);
+ const newPageSchema = keepUid ? schema : updateUidOfPageSchema(schema);
const pageSchemaUid = newPageSchema?.['x-uid'] || uid();
const newTabSchemaUid = uid();
const newTabSchemaName = uid();
diff --git a/packages/core/utils/package.json b/packages/core/utils/package.json
index 01e48f9a7e..f398205c5b 100644
--- a/packages/core/utils/package.json
+++ b/packages/core/utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/utils",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
diff --git a/packages/core/utils/src/__tests__/assign.test.ts b/packages/core/utils/src/__tests__/assign.test.ts
index b40c90150b..4c074df5b9 100644
--- a/packages/core/utils/src/__tests__/assign.test.ts
+++ b/packages/core/utils/src/__tests__/assign.test.ts
@@ -25,6 +25,20 @@ describe('merge strategy', () => {
filter: { a: 'a2' },
});
});
+ it('case 1-1', () => {
+ const obj = assign(
+ {
+ filter: { a: 'a2' },
+ },
+ {},
+ {
+ filter: 'andMerge',
+ },
+ );
+ expect(obj).toMatchObject({
+ filter: { a: 'a2' },
+ });
+ });
it('case 2', () => {
const obj = assign(
{
@@ -39,6 +53,20 @@ describe('merge strategy', () => {
filter: { a: 'a1' },
});
});
+ it('case 2-2', () => {
+ const obj = assign(
+ {},
+ {
+ filter: { a: 'a1' },
+ },
+ {
+ filter: 'andMerge',
+ },
+ );
+ expect(obj).toMatchObject({
+ filter: { a: 'a1' },
+ });
+ });
it('case 3', () => {
const obj = assign(
{
@@ -55,6 +83,22 @@ describe('merge strategy', () => {
filter: { a: 'a1' },
});
});
+ it('case 3-2', () => {
+ const obj = assign(
+ {
+ filter: undefined,
+ },
+ {
+ filter: { a: 'a1' },
+ },
+ {
+ filter: 'andMerge',
+ },
+ );
+ expect(obj).toMatchObject({
+ filter: { a: 'a1' },
+ });
+ });
it('case 4', () => {
const obj = assign(
{
@@ -120,6 +164,50 @@ describe('merge strategy', () => {
filter: { a: 'a1' },
});
});
+ it('case 1', () => {
+ const obj = assign(
+ {
+ filter: { a: 'a2' },
+ },
+ {},
+ {
+ filter: 'orMerge',
+ },
+ );
+ expect(obj).toMatchObject({
+ filter: { a: 'a2' },
+ });
+ });
+ it('case 2', () => {
+ const obj = assign(
+ {},
+ {
+ filter: { a: 'a1' },
+ },
+ {
+ filter: 'orMerge',
+ },
+ );
+ expect(obj).toMatchObject({
+ filter: { a: 'a1' },
+ });
+ });
+ it('case 3', () => {
+ const obj = assign(
+ {
+ filter: undefined,
+ },
+ {
+ filter: { a: 'a1' },
+ },
+ {
+ filter: 'orMerge',
+ },
+ );
+ expect(obj).toMatchObject({
+ filter: { a: 'a1' },
+ });
+ });
it('case 4', () => {
const obj = assign(
{
@@ -171,6 +259,20 @@ describe('merge strategy', () => {
key1: ['val1'],
});
});
+ it('case 2', () => {
+ const obj = assign(
+ {},
+ {
+ key1: ['val1'],
+ },
+ {
+ key1: 'intersect',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: ['val1'],
+ });
+ });
it('case 3', () => {
const obj = assign(
{},
@@ -185,6 +287,20 @@ describe('merge strategy', () => {
key1: ['val2'],
});
});
+ it('case 3', () => {
+ const obj = assign(
+ {
+ key1: ['val2'],
+ },
+ {},
+ {
+ key1: 'intersect',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: ['val2'],
+ });
+ });
it('case 4', () => {
const obj = assign(
{
@@ -236,6 +352,20 @@ describe('merge strategy', () => {
key1: ['val1', 'val2'],
});
});
+ it('case 2', () => {
+ const obj = assign(
+ {},
+ {
+ key1: ['val1'],
+ },
+ {
+ key1: 'union',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: ['val1'],
+ });
+ });
it('case 2', () => {
const obj = assign(
{
@@ -264,6 +394,20 @@ describe('merge strategy', () => {
key1: ['val2'],
});
});
+ it('case 3', () => {
+ const obj = assign(
+ {
+ key1: ['val2'],
+ },
+ {},
+ {
+ key1: 'union',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: ['val2'],
+ });
+ });
it('case 4', () => {
const obj = assign(
{
@@ -315,6 +459,34 @@ describe('merge strategy', () => {
key1: 'val1 + val2',
});
});
+ it('case 2', () => {
+ const obj = assign(
+ {
+ filter: { a: 'a2' },
+ },
+ {},
+ {
+ filter: () => '123',
+ },
+ );
+ expect(obj).toMatchObject({
+ filter: '123',
+ });
+ });
+ it('case 3', () => {
+ const obj = assign(
+ {},
+ {
+ filter: { a: 'a2' },
+ },
+ {
+ filter: () => '123',
+ },
+ );
+ expect(obj).toMatchObject({
+ filter: '123',
+ });
+ });
});
describe('merge', () => {
@@ -336,6 +508,129 @@ describe('merge strategy', () => {
});
});
+ describe('overwrite', () => {
+ it('case 1', () => {
+ const obj = assign(
+ {
+ key1: 'a',
+ },
+ {
+ key1: 'b',
+ },
+ {
+ key1: 'overwrite',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: 'b',
+ });
+ });
+ it('case 2', () => {
+ const obj = assign(
+ {
+ key1: 'a',
+ },
+ {},
+ {
+ key1: 'overwrite',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: 'a',
+ });
+ });
+ it('case 3', () => {
+ const obj = assign(
+ {},
+ {
+ key1: 'a',
+ },
+ {
+ key1: 'overwrite',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: 'a',
+ });
+ });
+ it('case 4', () => {
+ const obj = assign(
+ {
+ key1: 'a',
+ },
+ {
+ key1: undefined,
+ },
+ {
+ key1: 'overwrite',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: 'a',
+ });
+ });
+ it('case 5', () => {
+ const obj = assign(
+ {
+ key1: 'a',
+ },
+ {
+ key1: null,
+ },
+ {
+ key1: 'overwrite',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: null,
+ });
+ });
+ it('case 6', () => {
+ const obj = assign(
+ {
+ key1: 'a',
+ },
+ {
+ key1: '',
+ },
+ {
+ key1: 'overwrite',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: '',
+ });
+ });
+ it('case 7', () => {
+ const obj = assign(
+ {
+ key1: 'a,b,c',
+ },
+ {},
+ {
+ key1: 'overwrite',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: ['a', 'b', 'c'],
+ });
+ });
+ it('case 8', () => {
+ const obj = assign(
+ {},
+ {
+ key1: 'a,b,c',
+ },
+ {
+ key1: 'overwrite',
+ },
+ );
+ expect(obj).toMatchObject({
+ key1: ['a', 'b', 'c'],
+ });
+ });
+ });
+
describe('default = deepmerge', () => {
it('case 1', () => {
const obj = assign(
@@ -416,4 +711,42 @@ describe('merge strategy', () => {
});
});
});
+
+ describe('source is empty', () => {
+ it('case 1', () => {
+ const obj = assign(
+ {
+ resourceName: 'uiSchemas',
+ resourceIndex: 'n0jylid5rqa',
+ actionName: 'getJsonSchema',
+ values: {},
+ sort: 'id',
+ fields: ['id'],
+ except: ['id'],
+ whitelist: ['id1'],
+ blacklist: ['id2'],
+ },
+ {},
+ {
+ filter: 'andMerge',
+ fields: 'intersect',
+ except: 'union',
+ whitelist: 'intersect',
+ blacklist: 'intersect',
+ sort: 'overwrite',
+ },
+ );
+ expect(obj).toMatchObject({
+ resourceName: 'uiSchemas',
+ resourceIndex: 'n0jylid5rqa',
+ actionName: 'getJsonSchema',
+ values: {},
+ sort: 'id',
+ fields: ['id'],
+ except: ['id'],
+ whitelist: ['id1'],
+ blacklist: ['id2'],
+ });
+ });
+ });
});
diff --git a/packages/core/utils/src/assign.ts b/packages/core/utils/src/assign.ts
index 306da77cba..81ad36b314 100644
--- a/packages/core/utils/src/assign.ts
+++ b/packages/core/utils/src/assign.ts
@@ -8,7 +8,7 @@
*/
import deepmerge from 'deepmerge';
-import lodash from 'lodash';
+import _ from 'lodash';
import { isPlainObject } from './common';
type MergeStrategyType = 'merge' | 'deepMerge' | 'overwrite' | 'andMerge' | 'orMerge' | 'intersect' | 'union';
@@ -32,8 +32,14 @@ function getKeys(target: any) {
export const mergeStrategies = new Map();
-mergeStrategies.set('overwrite', (_, y) => {
- if (typeof y === 'string') {
+mergeStrategies.set('overwrite', (x, y) => {
+ if (y === undefined) {
+ if (typeof x === 'string' && x.includes(',')) {
+ return x.split(',');
+ }
+ return x;
+ }
+ if (typeof y === 'string' && y.includes(',')) {
y = y.split(',');
}
return y;
@@ -88,7 +94,7 @@ mergeStrategies.set('union', (x, y) => {
if (typeof y === 'string') {
y = y.split(',');
}
- return lodash.uniq((x || []).concat(y || [])).filter(Boolean);
+ return _.uniq((x || []).concat(y || [])).filter(Boolean);
});
mergeStrategies.set('intersect', (x, y) =>
@@ -110,15 +116,22 @@ mergeStrategies.set('intersect', (x, y) =>
);
export function assign(target: any, source: any, strategies: MergeStrategies = {}) {
- getKeys(source).forEach((sourceKey) => {
+ const sourceKeys = getKeys(source);
+ const targetKeys = getKeys(target);
+ _.uniq([...sourceKeys, ...targetKeys]).forEach((sourceKey) => {
const strategy = strategies[sourceKey];
- let func = mergeStrategies.get('deepMerge');
+ let func: any;
if (typeof strategy === 'function') {
func = strategy;
} else if (typeof strategy === 'string' && mergeStrategies.has(strategy as any)) {
func = mergeStrategies.get(strategy as any);
}
- target[sourceKey] = func(target[sourceKey], source[sourceKey]);
+ if (func) {
+ target[sourceKey] = func(target[sourceKey], source[sourceKey]);
+ } else if (sourceKeys.includes(sourceKey)) {
+ const func = mergeStrategies.get('deepMerge');
+ target[sourceKey] = func(target[sourceKey], source[sourceKey]);
+ }
});
return target;
}
diff --git a/packages/core/utils/src/common.ts b/packages/core/utils/src/common.ts
index c4894af961..f6e9aef0b7 100644
--- a/packages/core/utils/src/common.ts
+++ b/packages/core/utils/src/common.ts
@@ -54,3 +54,47 @@ export const hasEmptyValue = (objOrArr: object | any[]) => {
export const nextTick = (fn: () => void) => {
setTimeout(fn);
};
+
+/**
+ * 通用树节点深度优先遍历函数
+ * @param {Object|Array} tree - 要遍历的树结构
+ * @param {Function} callback - 遍历每个节点时执行的回调函数,返回真值时停止遍历并返回当前节点
+ * @param {Object} options - 配置选项
+ * @param {string|Function} options.childrenKey - 子节点的属性名,默认为'children',也可以是一个函数
+ * @returns {any|undefined} - 找到的节点或undefined
+ */
+export function treeFind(
+ tree: T | T[],
+ callback: (node: T) => boolean,
+ options: {
+ childrenKey?: string | ((node: T) => T[] | undefined);
+ } = {},
+): T | undefined {
+ if (!tree) return undefined;
+
+ const { childrenKey = 'children' } = options;
+
+ // 处理根节点是数组的情况
+ const nodes = Array.isArray(tree) ? [...tree] : [tree];
+
+ // 深度优先搜索
+ for (const node of nodes) {
+ // 对当前节点调用回调函数
+ if (callback(node)) {
+ return node;
+ }
+
+ // 获取子节点
+ const children = typeof childrenKey === 'function' ? childrenKey(node) : (node as any)[childrenKey];
+
+ // 递归处理子节点
+ if (Array.isArray(children) && children.length > 0) {
+ const found = treeFind(children, callback, options);
+ if (found !== undefined) {
+ return found;
+ }
+ }
+ }
+
+ return undefined;
+}
diff --git a/packages/core/utils/src/date.ts b/packages/core/utils/src/date.ts
index d1cc4a5172..677591243f 100644
--- a/packages/core/utils/src/date.ts
+++ b/packages/core/utils/src/date.ts
@@ -69,12 +69,13 @@ export const toLocal = (value: dayjs.Dayjs) => {
};
const convertQuarterToFirstDay = (quarterStr) => {
- if (dayjs(quarterStr).isValid()) {
+ try {
const year = parseInt(quarterStr.slice(0, 4)); // 提取年份
const quarter = parseInt(quarterStr.slice(-1)); // 提取季度数字
return dayjs().quarter(quarter).year(year);
+ } catch (error) {
+ return null;
}
- return null;
};
const toMoment = (val: any, options?: Str2momentOptions) => {
diff --git a/packages/core/utils/src/dayjs.ts b/packages/core/utils/src/dayjs.ts
index 0a196ed23d..a780681112 100644
--- a/packages/core/utils/src/dayjs.ts
+++ b/packages/core/utils/src/dayjs.ts
@@ -10,6 +10,7 @@
import dayjs from 'dayjs';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
+import duration from 'dayjs/plugin/duration';
import IsBetween from 'dayjs/plugin/isBetween';
import IsSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
@@ -35,5 +36,6 @@ dayjs.extend(weekOfYear);
dayjs.extend(weekYear);
dayjs.extend(customParseFormat);
dayjs.extend(advancedFormat);
+dayjs.extend(duration);
export { dayjs };
diff --git a/packages/plugins/@nocobase/plugin-acl/package.json b/packages/plugins/@nocobase/plugin-acl/package.json
index 00c90f17a0..3ee0e40973 100644
--- a/packages/plugins/@nocobase/plugin-acl/package.json
+++ b/packages/plugins/@nocobase/plugin-acl/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "权限控制",
"description": "Based on roles, resources, and actions, access control can precisely manage interface configuration permissions, data operation permissions, menu access permissions, and plugin permissions.",
"description.zh-CN": "基于角色、资源和操作的权限控制,可以精确控制界面配置权限、数据操作权限、菜单访问权限、插件权限。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/acl",
diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/SwitchRole.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/SwitchRole.tsx
index 433fe17f30..5da9b132b8 100644
--- a/packages/plugins/@nocobase/plugin-acl/src/client/SwitchRole.tsx
+++ b/packages/plugins/@nocobase/plugin-acl/src/client/SwitchRole.tsx
@@ -17,14 +17,20 @@ import {
useCurrentRoleMode,
} from '@nocobase/client';
import { Divider } from 'antd';
+import _ from 'lodash';
export const SwitchRole = () => {
const { t } = useTranslation();
const api = useAPIClient();
- const roles = useCurrentRoles();
+ const roles = _.cloneDeep(useCurrentRoles());
const roleMode = useCurrentRoleMode();
const currentRole = roles.find((role) => role.name === api.auth.role)?.name;
-
+ if (roleMode === 'allow-use-union') {
+ roles.unshift({
+ name: '__union__',
+ title: t('Full permissions', { ns: 'acl' }),
+ });
+ }
// 当角色数量小于等于1 或者 是仅使用合并角色模式时,不显示切换角色选项
if (roles.length <= 1 || roleMode === 'only-use-union') {
return null;
diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/role-user.test.ts b/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/role-user.test.ts
index 3bbf312e2c..4c2a29b8ee 100644
--- a/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/role-user.test.ts
+++ b/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/role-user.test.ts
@@ -11,6 +11,8 @@ import Database, { BelongsToManyRepository } from '@nocobase/database';
import UsersPlugin from '@nocobase/plugin-users';
import { createMockServer, MockServer } from '@nocobase/test';
import jwt from 'jsonwebtoken';
+import { SystemRoleMode } from '../enum';
+import { UNION_ROLE_KEY } from '../constants';
describe('role', () => {
let api: MockServer;
@@ -20,7 +22,7 @@ describe('role', () => {
beforeEach(async () => {
api = await createMockServer({
- plugins: ['field-sort', 'users', 'acl', 'auth', 'data-source-manager', 'system-settings'],
+ plugins: ['field-sort', 'users', 'acl', 'auth', 'data-source-manager', 'system-settings', 'ui-schema-storage'],
});
db = api.db;
usersPlugin = api.getPlugin('users');
@@ -163,4 +165,77 @@ describe('role', () => {
expect(defaultRole['name']).toEqual('test2');
});
+
+ it('should set users default role is __union__', async () => {
+ const role = await db.getRepository('roles').create({
+ values: {
+ name: 'test',
+ title: 'test user',
+ allowConfigure: true,
+ },
+ });
+ const user = await db.getRepository('users').create({
+ values: {
+ token: '123',
+ roles: [role.name],
+ },
+ });
+
+ const repo = db.getRepository('systemSettings');
+ await repo.update({
+ filter: { id: 1 },
+ values: {
+ roleMode: SystemRoleMode.allowUseUnion,
+ },
+ });
+ const userToken = jwt.sign({ userId: user.get('id') }, 'test-key');
+ const response = await api
+ .agent()
+ .post('/users:setDefaultRole')
+ .send({
+ roleName: UNION_ROLE_KEY,
+ })
+ .set({
+ Authorization: `Bearer ${userToken}`,
+ 'X-Authenticator': 'basic',
+ });
+
+ expect(response.statusCode).toEqual(200);
+ let userRole = await db.getRepository('rolesUsers').findOne({ where: { userId: user.get('id'), default: true } });
+ expect(userRole.roleName).toEqual(UNION_ROLE_KEY);
+
+ // switch
+ const response1 = await api
+ .agent()
+ .post('/users:setDefaultRole')
+ .send({
+ roleName: role.name,
+ })
+ .set({
+ Authorization: `Bearer ${userToken}`,
+ 'X-Authenticator': 'basic',
+ });
+
+ expect(response1.statusCode).toEqual(200);
+ userRole = await db.getRepository('rolesUsers').findOne({ where: { userId: user.get('id'), default: true } });
+ expect(userRole.roleName).toEqual(role.name);
+
+ const response2 = await api
+ .agent()
+ .post('/users:setDefaultRole')
+ .send({
+ roleName: UNION_ROLE_KEY,
+ })
+ .set({
+ Authorization: `Bearer ${userToken}`,
+ 'X-Authenticator': 'basic',
+ });
+
+ expect(response2.statusCode).toEqual(200);
+ userRole = await db.getRepository('rolesUsers').findOne({ where: { userId: user.get('id'), default: true } });
+ expect(userRole.roleName).toEqual(UNION_ROLE_KEY);
+ const agent = await api.agent().login(user);
+ const response3 = await agent.resource('roles').check();
+ expect(response3.statusCode).toEqual(200);
+ });
});
diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/union-role.test.ts b/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/union-role.test.ts
index 36ad2cbf17..33a19f9cf5 100644
--- a/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/union-role.test.ts
+++ b/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/union-role.test.ts
@@ -51,7 +51,11 @@ describe('union role: full permissions', async () => {
roles: [role1.name, role2.name],
},
});
-
+ await rootAgent.resource('roles').setSystemRoleMode({
+ values: {
+ roleMode: SystemRoleMode.allowUseUnion,
+ },
+ });
agent = await app.agent().login(user, UNION_ROLE_KEY);
});
@@ -415,15 +419,15 @@ describe('union role: full permissions', async () => {
const rootAgent = await app.agent().login(rootUser);
let rolesResponse = await agent.resource('roles').check();
expect(rolesResponse.status).toBe(200);
- expect(rolesResponse.body.data.roleMode).toStrictEqual(SystemRoleMode.default);
+ expect(rolesResponse.body.data.roleMode).toStrictEqual(SystemRoleMode.allowUseUnion);
await rootAgent.resource('roles').setSystemRoleMode({
values: {
- roleMode: SystemRoleMode.allowUseUnion,
+ roleMode: SystemRoleMode.default,
},
});
rolesResponse = await agent.resource('roles').check();
expect(rolesResponse.status).toBe(200);
- expect(rolesResponse.body.data.roleMode).toStrictEqual(SystemRoleMode.allowUseUnion);
+ expect(rolesResponse.body.data.roleMode).toStrictEqual(SystemRoleMode.default);
});
it(`should response no permission when createdById field is missing in data tables`, async () => {
@@ -501,4 +505,175 @@ describe('union role: full permissions', async () => {
expect(getRolesResponse.statusCode).toBe(200);
expect(getRolesResponse.body.meta.allowedActions.update.length).toBe(0);
});
+
+ it('should login successfully when use __union__ role in allowUseUnion mode #1906', async () => {
+ const rootAgent = await app.agent().login(rootUser);
+ await rootAgent.resource('roles').setSystemRoleMode({
+ values: {
+ roleMode: SystemRoleMode.allowUseUnion,
+ },
+ });
+ agent = await app.agent().login(user);
+ const createRoleResponse = await agent.resource('roles').check();
+ expect(createRoleResponse.statusCode).toBe(200);
+ });
+
+ it('should currentRole not be __union__ when default role mode #1907', async () => {
+ const rootAgent = await app.agent().login(rootUser);
+ await rootAgent.resource('roles').setSystemRoleMode({
+ values: {
+ roleMode: SystemRoleMode.default,
+ },
+ });
+ agent = await app.agent().login(user, UNION_ROLE_KEY);
+ const createRoleResponse = await agent.resource('roles').check();
+ expect(createRoleResponse.statusCode).toBe(200);
+ expect(createRoleResponse.body.data.role).not.toBe(UNION_ROLE_KEY);
+ });
+
+ it('should general action permissions override specific resource permissions when using union role #1924', async () => {
+ const rootAgent = await app.agent().login(rootUser);
+ await rootAgent
+ .post(`/dataSources/main/roles:update`)
+ .query({
+ filterByTk: role1.name,
+ })
+ .send({
+ roleName: role1.name,
+ strategy: {
+ actions: ['view'],
+ },
+ dataSourceKey: 'main',
+ });
+
+ const ownDataSourceScopeRole = await db.getRepository('dataSourcesRolesResourcesScopes').findOne({
+ where: {
+ key: 'own',
+ dataSourceKey: 'main',
+ },
+ });
+ const scopeFields = ['id', 'createdBy', 'createdById'];
+ const dataSourceResourcesResponse = await rootAgent
+ .post(`/roles/${role2.name}/dataSourceResources:create`)
+ .query({
+ filterByTk: 'users',
+ filter: {
+ dataSourceKey: 'main',
+ name: 'users',
+ },
+ })
+ .send({
+ usingActionsConfig: true,
+ actions: [
+ {
+ name: 'view',
+ fields: scopeFields,
+ scope: {
+ id: ownDataSourceScopeRole.id,
+ createdAt: '2025-02-19T08:57:17.385Z',
+ updatedAt: '2025-02-19T08:57:17.385Z',
+ key: 'own',
+ dataSourceKey: 'main',
+ name: '{{t("Own records")}}',
+ resourceName: null,
+ scope: {
+ createdById: '{{ ctx.state.currentUser.id }}',
+ },
+ },
+ },
+ ],
+ name: 'users',
+ dataSourceKey: 'main',
+ });
+ expect(dataSourceResourcesResponse.statusCode).toBe(200);
+
+ agent = await app.agent().login(user, UNION_ROLE_KEY);
+ const rolesResponse = await agent.resource('roles').check();
+ expect(rolesResponse.status).toBe(200);
+ expect(rolesResponse.body.data.actions['users:view']).toStrictEqual({});
+ });
+
+ it('should verify actions configuration for union role with specific scopes', async () => {
+ const rootAgent = await app.agent().login(rootUser);
+ await rootAgent
+ .post(`/dataSources/main/roles:update`)
+ .query({
+ filterByTk: role1.name,
+ })
+ .send({
+ roleName: role1.name,
+ strategy: {
+ actions: ['view', 'create:own', 'update'],
+ },
+ dataSourceKey: 'main',
+ });
+
+ const ownDataSourceScopeRole = await db.getRepository('dataSourcesRolesResourcesScopes').findOne({
+ where: {
+ key: 'own',
+ dataSourceKey: 'main',
+ },
+ });
+ const scopeFields = ['id', 'createdBy', 'createdById'];
+ const dataSourceResourcesResponse = await rootAgent
+ .post(`/roles/${role2.name}/dataSourceResources:create`)
+ .query({
+ filterByTk: 'users',
+ filter: {
+ dataSourceKey: 'main',
+ name: 'users',
+ },
+ })
+ .send({
+ usingActionsConfig: true,
+ actions: [
+ {
+ name: 'view',
+ fields: scopeFields,
+ scope: {
+ id: ownDataSourceScopeRole.id,
+ createdAt: '2025-02-19T08:57:17.385Z',
+ updatedAt: '2025-02-19T08:57:17.385Z',
+ key: 'own',
+ dataSourceKey: 'main',
+ name: '{{t("Own records")}}',
+ resourceName: null,
+ scope: {
+ createdById: '{{ ctx.state.currentUser.id }}',
+ },
+ },
+ },
+ {
+ name: 'create',
+ fields: scopeFields,
+ scope: {
+ id: ownDataSourceScopeRole.id,
+ createdAt: '2025-02-19T08:57:17.385Z',
+ updatedAt: '2025-02-19T08:57:17.385Z',
+ key: 'own',
+ dataSourceKey: 'main',
+ name: '{{t("Own records")}}',
+ resourceName: null,
+ scope: {
+ createdById: '{{ ctx.state.currentUser.id }}',
+ },
+ },
+ },
+ ],
+ name: 'users',
+ dataSourceKey: 'main',
+ });
+ expect(dataSourceResourcesResponse.statusCode).toBe(200);
+
+ agent = await app.agent().login(user, UNION_ROLE_KEY);
+ const rolesResponse = await agent.resource('roles').check();
+ expect(rolesResponse.status).toBe(200);
+ expect(rolesResponse.body.data.actions).toHaveProperty('users:create');
+ expect(rolesResponse.body.data.actions).toHaveProperty('users:view');
+ expect(rolesResponse.body.data.actions['users:view']).toStrictEqual({});
+ expect(rolesResponse.body.data.actions).not.toHaveProperty('users:create:own');
+ expect(rolesResponse.body.data.actions['users:create']).toHaveProperty('filter');
+ expect(rolesResponse.body.data.actions['users:create']).toHaveProperty('whitelist');
+ expect(rolesResponse.body.data.actions['users:update']).toStrictEqual({});
+ });
});
diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/actions/user-setDefaultRole.ts b/packages/plugins/@nocobase/plugin-acl/src/server/actions/user-setDefaultRole.ts
index 5ee44fca14..093f3f7fc2 100644
--- a/packages/plugins/@nocobase/plugin-acl/src/server/actions/user-setDefaultRole.ts
+++ b/packages/plugins/@nocobase/plugin-acl/src/server/actions/user-setDefaultRole.ts
@@ -29,25 +29,47 @@ export async function setDefaultRole(ctx: Context, next: Next) {
const repository = db.getRepository('rolesUsers');
await db.sequelize.transaction(async (transaction) => {
- await repository.update({
+ const currentUserDefaultRole = await repository.findOne({
filter: {
userId: currentUser.id,
- },
- values: {
- default: false,
- },
- transaction,
- });
- await repository.update({
- filter: {
- userId: currentUser.id,
- roleName,
- },
- values: {
default: true,
},
transaction,
});
+
+ if (currentUserDefaultRole?.roleName === roleName) {
+ return;
+ }
+
+ if (currentUserDefaultRole) {
+ await repository.model.update(
+ { default: false },
+ { where: { userId: currentUser.id, roleName: currentUserDefaultRole.roleName }, transaction },
+ );
+ }
+
+ const targetUserRole = await repository.findOne({
+ filter: {
+ userId: currentUser.id,
+ roleName,
+ },
+ transaction,
+ });
+ let model;
+ if (targetUserRole) {
+ await repository.model.update({ default: true }, { where: { userId: currentUser.id, roleName }, transaction });
+ model = targetUserRole.set('default', true);
+ } else {
+ model = await repository.create({
+ values: {
+ userId: currentUser.id,
+ roleName,
+ default: true,
+ },
+ transaction,
+ });
+ }
+ db.emitAsync('rolesUsers.afterSave', model);
});
ctx.body = 'ok';
diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/middlewares/setCurrentRole.ts b/packages/plugins/@nocobase/plugin-acl/src/server/middlewares/setCurrentRole.ts
index 49b8acaf14..4d06502ce3 100644
--- a/packages/plugins/@nocobase/plugin-acl/src/server/middlewares/setCurrentRole.ts
+++ b/packages/plugins/@nocobase/plugin-acl/src/server/middlewares/setCurrentRole.ts
@@ -14,7 +14,7 @@ import { UNION_ROLE_KEY } from '../constants';
import { SystemRoleMode } from '../enum';
export async function setCurrentRole(ctx: Context, next) {
- const currentRole = ctx.get('X-Role');
+ let currentRole = ctx.get('X-Role');
if (currentRole === 'anonymous') {
ctx.state.currentRole = currentRole;
@@ -47,9 +47,12 @@ export async function setCurrentRole(ctx: Context, next) {
roles.forEach((role: any) => rolesMap.set(role.name, role));
const userRoles = Array.from(rolesMap.values());
ctx.state.currentUser.roles = userRoles;
- const systemSettings = await ctx.db.getRepository('systemSettings').findOne();
- const roleMode = systemSettings?.get('roleMode') || SystemRoleMode.default;
- if (ctx.state.currentRole === UNION_ROLE_KEY && roleMode === SystemRoleMode.default) {
+ const systemSettings = (await cache.wrap(`app:systemSettings`, () =>
+ ctx.db.getRepository('systemSettings').findOne({ raw: true }),
+ )) as Model;
+ const roleMode = systemSettings?.roleMode || SystemRoleMode.default;
+ if ([currentRole, ctx.state.currentRole].includes(UNION_ROLE_KEY) && roleMode === SystemRoleMode.default) {
+ currentRole = userRoles[0].name;
ctx.state.currentRole = userRoles[0].name;
ctx.headers['x-role'] = userRoles[0].name;
} else if (roleMode === SystemRoleMode.onlyUseUnion) {
@@ -57,12 +60,6 @@ export async function setCurrentRole(ctx: Context, next) {
ctx.headers['x-role'] = UNION_ROLE_KEY;
ctx.state.currentRoles = userRoles.map((role) => role.name);
return next();
- } else if (roleMode === SystemRoleMode.allowUseUnion) {
- userRoles.unshift({
- name: UNION_ROLE_KEY,
- title: ctx.t('Full permissions', { ns: 'acl' }),
- });
- ctx.state.currentUser.roles = userRoles;
}
if (currentRole === UNION_ROLE_KEY) {
@@ -84,11 +81,13 @@ export async function setCurrentRole(ctx: Context, next) {
}
// 2. If the X-Role is not set, or the X-Role does not belong to the user, use the default role
if (!role) {
- const defaultRole = userRoles.find((role) => role?.rolesUsers?.default);
- role = (defaultRole || userRoles[0])?.name;
+ const defaultRoleModel = await cache.wrap(`roles:${ctx.state.currentUser.id}:defaultRole`, () =>
+ ctx.db.getRepository('rolesUsers').findOne({ where: { userId: ctx.state.currentUser.id, default: true } }),
+ );
+ role = defaultRoleModel?.roleName || userRoles[0]?.name;
}
ctx.state.currentRole = role;
- ctx.state.currentRoles = [role];
+ ctx.state.currentRoles = role === UNION_ROLE_KEY ? [userRoles[0]?.name] : [role];
if (!ctx.state.currentRoles.length) {
return ctx.throw(401, {
code: 'ROLE_NOT_FOUND_ERR',
diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/server.ts b/packages/plugins/@nocobase/plugin-acl/src/server/server.ts
index 142bde0ebd..96dc233cb1 100644
--- a/packages/plugins/@nocobase/plugin-acl/src/server/server.ts
+++ b/packages/plugins/@nocobase/plugin-acl/src/server/server.ts
@@ -341,10 +341,16 @@ export class PluginACLServer extends Plugin {
this.app.db.on('rolesUsers.afterSave', async (model) => {
const cache = this.app.cache as Cache;
await cache.del(`roles:${model.get('userId')}`);
+ await cache.del(`roles:${model.get('userId')}:defaultRole`);
+ });
+ this.app.db.on('systemSettings.afterSave', async (model) => {
+ const cache = this.app.cache as Cache;
+ await cache.del(`app:systemSettings`);
});
this.app.db.on('rolesUsers.afterDestroy', async (model) => {
const cache = this.app.cache as Cache;
await cache.del(`roles:${model.get('userId')}`);
+ await cache.del(`roles:${model.get('userId')}:defaultRole`);
});
const writeRolesToACL = async (app, options) => {
diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/package.json b/packages/plugins/@nocobase/plugin-action-bulk-edit/package.json
index 5428d9bd04..6edc7a09c0 100644
--- a/packages/plugins/@nocobase/plugin-action-bulk-edit/package.json
+++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-action-bulk-edit",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-bulk-edit",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-edit",
diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditAction.Settings.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditAction.Settings.tsx
index 713d5f0c1b..b126f527e8 100644
--- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditAction.Settings.tsx
+++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditAction.Settings.tsx
@@ -21,9 +21,10 @@ import {
SecondConFirm,
AfterSuccess,
RefreshDataBlockRequest,
+ SchemaSettingsLinkageRules,
+ useDataBlockProps,
} from '@nocobase/client';
import { ModalProps } from 'antd';
-import { isValid } from '@formily/shared';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -96,6 +97,16 @@ export const deprecatedBulkEditActionSettings = new SchemaSettings({
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'openMode',
Component: SchemaInitializerOpenModeSchemaItems,
@@ -138,6 +149,16 @@ export const bulkEditActionSettings = new SchemaSettings({
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'openMode',
Component: SchemaInitializerOpenModeSchemaItems,
@@ -158,6 +179,7 @@ export const bulkEditActionSettings = new SchemaSettings({
name: 'updateMode',
Component: UpdateMode,
},
+
{
name: 'remove',
sort: 100,
@@ -191,6 +213,17 @@ export const bulkEditFormSubmitActionSettings = new SchemaSettings({
name: 'afterSuccessfulSubmission',
Component: AfterSuccess,
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'refreshDataBlockRequest',
Component: RefreshDataBlockRequest,
diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/__e2e__/refresh.test.ts b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/__e2e__/refresh.test.ts
new file mode 100644
index 0000000000..8cc2b184c2
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/__e2e__/refresh.test.ts
@@ -0,0 +1,34 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import { expect, test } from '@nocobase/test/e2e';
+import { afterConfiguringTheModalWhenReopeningItTheContentShouldPersist } from './utils';
+
+test.describe('refresh', () => {
+ test('After configuring the modal, when reopening it, the content should persist', async ({ mockPage, page }) => {
+ await mockPage(afterConfiguringTheModalWhenReopeningItTheContentShouldPersist).goto();
+
+ // 1. 点击 Bulk edit 按钮,打开弹窗
+ await page.getByLabel('action-Action-Bulk edit-').click();
+
+ // 2. 新增一个表单区块
+ await page.getByLabel('schema-initializer-Grid-popup').hover();
+ await page.getByRole('menuitem', { name: 'form Form' }).click();
+
+ // 3. 新增一个名为 Nickname 的字段
+ await page.getByLabel('schema-initializer-Grid-bulkEditForm:configureFields-users').hover();
+ await page.getByRole('menuitem', { name: 'Nickname' }).click();
+
+ // 4. 关闭弹窗,然后再打开,刚才新增的字段应该还在
+ await page.getByLabel('drawer-Action.Container-users-Bulk edit-mask').click();
+ await page.getByLabel('action-Action-Bulk edit-').click();
+ await expect(page.getByLabel('block-item-BulkEditField-').getByText('Nickname')).toBeVisible();
+ await page.getByLabel('block-item-BulkEditField-').click();
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/__e2e__/utils.ts b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/__e2e__/utils.ts
index 3e67dc3ad2..d2068a7c82 100644
--- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/__e2e__/utils.ts
+++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/__e2e__/utils.ts
@@ -1243,3 +1243,388 @@ export const theAddBlockButtonInDrawerShouldBeVisible = {
'x-index': 1,
},
};
+export const afterConfiguringTheModalWhenReopeningItTheContentShouldPersist = {
+ pageSchema: {
+ type: 'void',
+ 'x-component': 'Page',
+ name: 'rjzvy4bmawn',
+ 'x-uid': '1rs9caegbf2',
+ 'x-async': false,
+ properties: {
+ tab: {
+ type: 'void',
+ 'x-component': 'Grid',
+ 'x-initializer': 'page:addBlock',
+ properties: {
+ bmsmf8futai: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'x84k7qs6jko',
+ 'x-async': false,
+ 'x-index': 4,
+ },
+ noe2oca30hc: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 't7jxa830ps6',
+ 'x-async': false,
+ 'x-index': 5,
+ },
+ w2hnq7rau9p: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': '0fjjtg8z7ws',
+ 'x-async': false,
+ 'x-index': 7,
+ },
+ fcfs4oot86g: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'nklv7lonpgn',
+ 'x-async': false,
+ 'x-index': 8,
+ },
+ i22fydav355: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'fz4g6cr9jvr',
+ 'x-async': false,
+ 'x-index': 10,
+ },
+ row_6u7y7uccrvz: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-index': 12,
+ 'x-uid': '7tzumo4nec7',
+ 'x-async': false,
+ },
+ higfesvgj7g: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': '8hpa6qf3sez',
+ 'x-async': false,
+ 'x-index': 13,
+ },
+ '37myao9n0wc': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'uw1dp2qxd3y',
+ 'x-async': false,
+ 'x-index': 14,
+ },
+ uvfd76q4ye9: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'nc56fu33m42',
+ 'x-async': false,
+ 'x-index': 15,
+ },
+ miidizeqgot: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'jf4qarrcs0z',
+ 'x-async': false,
+ 'x-index': 16,
+ },
+ hxmr87i5imu: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'l3kdiqd9a7k',
+ 'x-async': false,
+ 'x-index': 17,
+ },
+ pa8dwdi4h5a: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'uz5wcet83qn',
+ 'x-async': false,
+ 'x-index': 18,
+ },
+ pno0a05tbnp: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'b4bakhhasp3',
+ 'x-async': false,
+ 'x-index': 19,
+ },
+ uj09g5xgnr1: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'qks035fnfl6',
+ 'x-async': false,
+ 'x-index': 20,
+ },
+ giobcwj316k: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ 'x-uid': 'awwsb89nyso',
+ 'x-async': false,
+ 'x-index': 22,
+ },
+ oznewtbvuyw: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.6.11',
+ properties: {
+ bwtax0bnnp3: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.6.11',
+ properties: {
+ c0bypj7wg5q: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-decorator': 'TableBlockProvider',
+ 'x-acl-action': 'users:list',
+ 'x-use-decorator-props': 'useTableBlockDecoratorProps',
+ 'x-decorator-props': {
+ collection: 'users',
+ dataSource: 'main',
+ action: 'list',
+ params: {
+ pageSize: 20,
+ },
+ rowKey: 'id',
+ showIndex: true,
+ dragSort: false,
+ },
+ 'x-toolbar': 'BlockSchemaToolbar',
+ 'x-settings': 'blockSettings:table',
+ 'x-component': 'CardItem',
+ 'x-filter-targets': [],
+ 'x-app-version': '1.6.11',
+ properties: {
+ actions: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-initializer': 'table:configureActions',
+ 'x-component': 'ActionBar',
+ 'x-component-props': {
+ style: {
+ marginBottom: 'var(--nb-spacing)',
+ },
+ },
+ 'x-app-version': '1.6.11',
+ properties: {
+ '1dlvhzr308c': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ title: '{{t("Bulk edit")}}',
+ 'x-component': 'Action',
+ 'x-action': 'customize:bulkEdit',
+ 'x-action-settings': {
+ updateMode: 'selected',
+ },
+ 'x-component-props': {
+ openMode: 'drawer',
+ icon: 'EditOutlined',
+ },
+ 'x-align': 'right',
+ 'x-decorator': 'BulkEditActionDecorator',
+ 'x-toolbar': 'ActionSchemaToolbar',
+ 'x-settings': 'actionSettings:bulkEdit',
+ 'x-acl-action': 'update',
+ 'x-acl-action-props': {
+ skipScopeCheck: true,
+ },
+ 'x-app-version': '1.6.11',
+ properties: {
+ drawer: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ title: '{{t("Bulk edit")}}',
+ 'x-component': 'Action.Container',
+ 'x-component-props': {
+ className: 'nb-action-popup',
+ },
+ 'x-app-version': '1.6.11',
+ properties: {
+ tabs: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Tabs',
+ 'x-component-props': {},
+ 'x-initializer': 'popup:addTab',
+ 'x-initializer-props': {
+ gridInitializer: 'popup:bulkEdit:addBlock',
+ },
+ 'x-app-version': '1.6.11',
+ properties: {
+ tab1: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ title: '{{t("Bulk edit")}}',
+ 'x-component': 'Tabs.TabPane',
+ 'x-designer': 'Tabs.Designer',
+ 'x-component-props': {},
+ 'x-app-version': '1.6.11',
+ properties: {
+ grid: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid',
+ 'x-initializer': 'popup:bulkEdit:addBlock',
+ 'x-app-version': '1.6.11',
+ 'x-uid': '5ejbu8v5ol8',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'gfheiqtl7f7',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'if2rcx1dy2n',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'cxyi8q6lm3n',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'vbvf13xq15t',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'amlzm32jhwg',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ f232o2ds23n: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'array',
+ 'x-initializer': 'table:configureColumns',
+ 'x-component': 'TableV2',
+ 'x-use-component-props': 'useTableBlockProps',
+ 'x-component-props': {
+ rowKey: 'id',
+ rowSelection: {
+ type: 'checkbox',
+ },
+ },
+ 'x-app-version': '1.6.11',
+ properties: {
+ actions: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ title: '{{ t("Actions") }}',
+ 'x-action-column': 'actions',
+ 'x-decorator': 'TableV2.Column.ActionBar',
+ 'x-component': 'TableV2.Column',
+ 'x-toolbar': 'TableColumnSchemaToolbar',
+ 'x-initializer': 'table:configureItemActions',
+ 'x-settings': 'fieldSettings:TableColumn',
+ 'x-toolbar-props': {
+ initializer: 'table:configureItemActions',
+ },
+ 'x-app-version': '1.6.11',
+ properties: {
+ '153lpq30p5f': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-decorator': 'DndContext',
+ 'x-component': 'Space',
+ 'x-component-props': {
+ split: '|',
+ },
+ 'x-app-version': '1.6.11',
+ 'x-uid': '4mha1dmmyz9',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'afmceivuaf0',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'iu0xkmeuc5z',
+ 'x-async': false,
+ 'x-index': 2,
+ },
+ },
+ 'x-uid': 'ylor106s9ok',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'rl50hidu14n',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'xxhug2yumqf',
+ 'x-async': false,
+ 'x-index': 23,
+ },
+ },
+ name: 'h63eibc46on',
+ 'x-uid': 'u9g23o0ohgk',
+ 'x-async': true,
+ 'x-index': 1,
+ },
+ },
+ },
+};
diff --git a/packages/plugins/@nocobase/plugin-action-bulk-update/package.json b/packages/plugins/@nocobase/plugin-action-bulk-update/package.json
index 1627e5624b..b403878ce7 100644
--- a/packages/plugins/@nocobase/plugin-action-bulk-update/package.json
+++ b/packages/plugins/@nocobase/plugin-action-bulk-update/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-action-bulk-update",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-bulk-update",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-update",
diff --git a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/BulkUpdateAction.Settings.tsx b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/BulkUpdateAction.Settings.tsx
index 894faaaf7d..6bdc3c1623 100644
--- a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/BulkUpdateAction.Settings.tsx
+++ b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/BulkUpdateAction.Settings.tsx
@@ -20,6 +20,12 @@ import {
RefreshDataBlockRequest,
useAfterSuccessOptions,
useGlobalVariable,
+ BlocksSelector,
+ usePlugin,
+ SchemaSettingsLinkageRules,
+ useCollectionManager_deprecated,
+ useDataBlockProps,
+ useCollection_deprecated,
} from '@nocobase/client';
import { useTranslation } from 'react-i18next';
import React from 'react';
@@ -61,13 +67,19 @@ const useVariableProps = (environmentVariables) => {
fieldNames,
};
};
+
function AfterSuccess() {
const { dn } = useDesignable();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const environmentVariables = useGlobalVariable('$env');
+ const templatePlugin: any = usePlugin('@nocobase/plugin-block-template');
+ const isInBlockTemplateConfigPage = templatePlugin?.isInBlockTemplateConfigPage?.();
+
return (
useVariableProps(environmentVariables),
},
+ blocksToRefresh: {
+ type: 'array',
+ title: t('Refresh data blocks'),
+ 'x-decorator': 'FormItem',
+ 'x-use-decorator-props': () => {
+ return {
+ tooltip: t('After successful submission, the selected data blocks will be automatically refreshed.'),
+ };
+ },
+ 'x-component': BlocksSelector,
+ 'x-hidden': isInBlockTemplateConfigPage, // 模板配置页面暂不支持该配置
+ },
},
} as ISchema
}
@@ -141,6 +165,21 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection_deprecated();
+ const { association } = useDataBlockProps() || {};
+ const { getCollectionField } = useCollectionManager_deprecated();
+ const associationField = getCollectionField(association);
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ collectionName: associationField?.collectionName || name,
+ };
+ },
+ },
{
name: 'updateMode',
Component: UpdateMode,
diff --git a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/__e2e__/schemaSettings.test.ts b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/__e2e__/schemaSettings.test.ts
index 73bd22caac..c4cbfa3a5e 100644
--- a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/__e2e__/schemaSettings.test.ts
+++ b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/__e2e__/schemaSettings.test.ts
@@ -37,8 +37,8 @@ test.describe('data will be updated && Assign field values && after successful s
await page.getByRole('menuitem', { name: 'After successful submission' }).click();
await page.getByLabel('Manually close').check();
await page.getByLabel('Redirect to').check();
- await page.locator('input[type="text"]').click();
- await page.locator('input[type="text"]').fill('/admin/pm/list/local/');
+ await page.getByLabel('textbox').click();
+ await page.getByLabel('textbox').fill('/admin/pm/list/local/');
await page.getByRole('button', { name: 'OK', exact: true }).click();
await page.getByLabel('action-Action-Bulk update-customize:bulkUpdate-general-table').click();
const [request] = await Promise.all([
diff --git a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/utils.tsx b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/utils.tsx
index ac7253f2d0..c2bd868699 100644
--- a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/utils.tsx
+++ b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/utils.tsx
@@ -42,107 +42,113 @@ export const useCustomizeBulkUpdateActionProps = () => {
const localVariables = useLocalVariables();
return {
async onClick(e, callBack) {
- const {
- assignedValues: originalAssignedValues = {},
- onSuccess,
- updateMode,
- } = actionSchema?.['x-action-settings'] ?? {};
- actionField.data = field.data || {};
- actionField.data.loading = true;
- const selectedRecordKeys =
- tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {};
+ return new Promise(async (resolve) => {
+ const {
+ assignedValues: originalAssignedValues = {},
+ onSuccess,
+ updateMode,
+ } = actionSchema?.['x-action-settings'] ?? {};
+ actionField.data = field.data || {};
+ actionField.data.loading = true;
+ const selectedRecordKeys =
+ tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {};
- const assignedValues = {};
- const waitList = Object.keys(originalAssignedValues).map(async (key) => {
- const value = originalAssignedValues[key];
- const collectionField = getField(key);
+ const assignedValues = {};
+ const waitList = Object.keys(originalAssignedValues).map(async (key) => {
+ const value = originalAssignedValues[key];
+ const collectionField = getField(key);
- if (process.env.NODE_ENV !== 'production') {
- if (!collectionField) {
- throw new Error(`useCustomizeBulkUpdateActionProps: field "${key}" not found in collection "${name}"`);
+ if (process.env.NODE_ENV !== 'production') {
+ if (!collectionField) {
+ throw new Error(`useCustomizeBulkUpdateActionProps: field "${key}" not found in collection "${name}"`);
+ }
}
- }
- if (isVariable(value)) {
- const result = await variables?.parseVariable(value, localVariables).then(({ value }) => value);
- if (result) {
- assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
+ if (isVariable(value)) {
+ const result = await variables?.parseVariable(value, localVariables).then(({ value }) => value);
+ if (result) {
+ assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
+ }
+ } else if (value !== '') {
+ assignedValues[key] = value;
}
- } else if (value !== '') {
- assignedValues[key] = value;
- }
- });
- await Promise.all(waitList);
+ });
+ await Promise.all(waitList);
- modal.confirm({
- title: t('Bulk update', { ns: 'client' }),
- content:
- updateMode === 'selected'
- ? t('Update selected data?', { ns: 'client' })
- : t('Update all data?', { ns: 'client' }),
- async onOk() {
- const { filter } = service.params?.[0] ?? {};
- const updateData: { filter?: any; values: any; forceUpdate: boolean } = {
- values: { ...assignedValues },
- filter,
- forceUpdate: false,
- };
- if (updateMode === 'selected') {
- if (!selectedRecordKeys?.length) {
- message.error(t('Please select the records to be updated'));
+ modal.confirm({
+ title: t('Bulk update', { ns: 'client' }),
+ content:
+ updateMode === 'selected'
+ ? t('Update selected data?', { ns: 'client' })
+ : t('Update all data?', { ns: 'client' }),
+ async onOk() {
+ const { filter } = service.params?.[0] ?? {};
+ const updateData: { filter?: any; values: any; forceUpdate: boolean } = {
+ values: { ...assignedValues },
+ filter,
+ forceUpdate: false,
+ };
+ if (updateMode === 'selected') {
+ if (!selectedRecordKeys?.length) {
+ message.error(t('Please select the records to be updated'));
+ actionField.data.loading = false;
+ return;
+ }
+ updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRecordKeys } }] };
+ }
+ if (!updateData.filter) {
+ updateData.forceUpdate = true;
+ }
+ try {
+ await resource.update(updateData);
+ } catch (error) {
+ /* empty */
+ } finally {
actionField.data.loading = false;
+ }
+ if (callBack) {
+ callBack?.();
+ }
+ // service?.refresh?.();
+ if (!(resource instanceof TableFieldResource)) {
+ __parent?.service?.refresh?.();
+ }
+ if (!onSuccess?.successMessage) {
return;
}
- updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRecordKeys } }] };
- }
- if (!updateData.filter) {
- updateData.forceUpdate = true;
- }
- try {
- await resource.update(updateData);
- } catch (error) {
- /* empty */
- } finally {
- actionField.data.loading = false;
- }
- if (callBack) {
- callBack?.();
- }
- // service?.refresh?.();
- if (!(resource instanceof TableFieldResource)) {
- __parent?.service?.refresh?.();
- }
- if (!onSuccess?.successMessage) {
- return;
- }
- if (onSuccess?.manualClose) {
- modal.success({
- title: compile(onSuccess?.successMessage),
- onOk: async () => {
- if (onSuccess?.redirecting && onSuccess?.redirectTo) {
- if (isURL(onSuccess.redirectTo)) {
- window.location.href = onSuccess.redirectTo;
- } else {
- navigate(onSuccess.redirectTo);
+ if (onSuccess?.manualClose) {
+ modal.success({
+ title: compile(onSuccess?.successMessage),
+ onOk: async () => {
+ if (onSuccess?.redirecting && onSuccess?.redirectTo) {
+ if (isURL(onSuccess.redirectTo)) {
+ window.location.href = onSuccess.redirectTo;
+ } else {
+ navigate(onSuccess.redirectTo);
+ }
}
+ },
+ });
+ } else {
+ message.success(compile(onSuccess?.successMessage));
+ if (onSuccess?.redirecting && onSuccess?.redirectTo) {
+ if (isURL(onSuccess.redirectTo)) {
+ window.location.href = onSuccess.redirectTo;
+ } else {
+ navigate(onSuccess.redirectTo);
}
- },
- });
- } else {
- message.success(compile(onSuccess?.successMessage));
- if (onSuccess?.redirecting && onSuccess?.redirectTo) {
- if (isURL(onSuccess.redirectTo)) {
- window.location.href = onSuccess.redirectTo;
- } else {
- navigate(onSuccess.redirectTo);
}
}
- }
- },
- async onCancel() {
- actionField.data.loading = false;
- },
+
+ resolve();
+ },
+ async onCancel() {
+ actionField.data.loading = false;
+ resolve();
+ },
+ });
});
+
},
};
};
diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/package.json b/packages/plugins/@nocobase/plugin-action-custom-request/package.json
index d7d00289a2..31412d72b3 100644
--- a/packages/plugins/@nocobase/plugin-action-custom-request/package.json
+++ b/packages/plugins/@nocobase/plugin-action-custom-request/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-action-custom-request",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-custom-request",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-custom-request",
diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/__e2e__/schemaSettings.test.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/__e2e__/schemaSettings.test.ts
index 8a2b0756ed..13c03555de 100644
--- a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/__e2e__/schemaSettings.test.ts
+++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/__e2e__/schemaSettings.test.ts
@@ -25,9 +25,7 @@ test.describe('custom request action', () => {
await page.getByLabel('designer-schema-settings-CustomRequestAction-actionSettings:customRequest-').hover();
await page.getByRole('menuitem', { name: 'Edit button' }).click();
- // 应该只显示标题输入框
await expect(page.getByText('Button title')).toBeVisible();
- await expect(page.getByText('Button icon')).not.toBeVisible();
await expect(page.getByText('Button background color')).not.toBeVisible();
});
diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomRequestVariableOptions.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomRequestVariableOptions.ts
index 9980ba5625..bb68899bfb 100644
--- a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomRequestVariableOptions.ts
+++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomRequestVariableOptions.ts
@@ -15,10 +15,28 @@ import {
useCollectionRecordData,
useCompile,
useGlobalVariable,
+ useContextAssociationFields,
+ useTableBlockContext,
+ useCurrentPopupContext,
+ getStoredPopupContext,
+ useFormBlockContext,
} from '@nocobase/client';
import { useMemo } from 'react';
+import { isEmpty } from 'lodash';
import { useTranslation } from '../locale';
+const useIsShowTableSelectRecord = () => {
+ const { params } = useCurrentPopupContext();
+ const recordData = useCollectionRecordData();
+ const tableBlockContextBasicValue = useTableBlockContext();
+ if (recordData) {
+ return false;
+ }
+
+ const popupTableBlockContext = getStoredPopupContext(params?.popupuid)?.tableBlockContext;
+ return !isEmpty(popupTableBlockContext) || !isEmpty(tableBlockContextBasicValue);
+};
+
export const useCustomRequestVariableOptions = () => {
const collection = useCollection_deprecated();
const { t } = useTranslation();
@@ -27,10 +45,13 @@ export const useCustomRequestVariableOptions = () => {
const compile = useCompile();
const recordData = useCollectionRecordData();
const { name: blockType } = useBlockContext() || {};
+ const { form } = useFormBlockContext();
const [fields, userFields] = useMemo(() => {
return [compile(fieldsOptions), compile(userFieldOptions)];
}, [fieldsOptions, userFieldOptions]);
const environmentVariables = useGlobalVariable('$env');
+ const contextVariable = useContextAssociationFields({ maxDepth: 2, contextCollectionName: collection.name });
+ const shouldShowTableSelectVariable = useIsShowTableSelectRecord();
return useMemo(() => {
return [
environmentVariables,
@@ -39,7 +60,7 @@ export const useCustomRequestVariableOptions = () => {
title: t('Current record', { ns: 'client' }),
children: [...fields],
},
- blockType === 'form' && {
+ (blockType === 'form' || form) && {
name: '$nForm',
title: t('Current form', { ns: 'client' }),
children: [...fields],
@@ -59,6 +80,7 @@ export const useCustomRequestVariableOptions = () => {
title: 'API token',
children: null,
},
+ shouldShowTableSelectVariable && { ...contextVariable, name: '$nSelectedRecord', title: contextVariable.label },
].filter(Boolean);
- }, [recordData, t, fields, blockType, userFields]);
+ }, [recordData, t, fields, blockType, userFields, shouldShowTableSelectVariable]);
};
diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts
index a1580495e9..93739c9c74 100644
--- a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts
+++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts
@@ -16,6 +16,8 @@ import {
useCompile,
useDataSourceKey,
useNavigateNoUpdate,
+ useBlockRequestContext,
+ useContextVariable,
} from '@nocobase/client';
import { isURL } from '@nocobase/utils/client';
import { App } from 'antd';
@@ -25,18 +27,21 @@ export const useCustomizeRequestActionProps = () => {
const apiClient = useAPIClient();
const navigate = useNavigateNoUpdate();
const actionSchema = useFieldSchema();
+ const { field } = useBlockRequestContext();
const compile = useCompile();
const form = useForm();
const { name: blockType } = useBlockContext() || {};
- // const { getPrimaryKey } = useCollection_deprecated();
const recordData = useCollectionRecordData();
const fieldSchema = useFieldSchema();
const actionField = useField();
const { setVisible } = useActionContext();
const { modal, message } = App.useApp();
const dataSourceKey = useDataSourceKey();
+ const { ctx } = useContextVariable();
+
return {
async onClick(e?, callBack?) {
+ const selectedRecord = field.data?.selectedRowData ? field.data?.selectedRowData : ctx;
const { skipValidator, onSuccess } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
const xAction = actionSchema?.['x-action'];
@@ -58,12 +63,11 @@ export const useCustomizeRequestActionProps = () => {
method: 'POST',
data: {
currentRecord: {
- // id: record[getPrimaryKey()],
- // appends: result.params[0]?.appends,
dataSourceKey,
data: currentRecordData,
},
$nForm: blockType === 'form' ? form.values : undefined,
+ $nSelectedRecord: selectedRecord,
},
responseType: fieldSchema['x-response-type'] === 'stream' ? 'blob' : 'json',
});
diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/schemaSettings.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/schemaSettings.ts
index ef4c43deb7..2a872ac122 100644
--- a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/schemaSettings.ts
+++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/schemaSettings.ts
@@ -20,6 +20,8 @@ import {
useCollectionRecord,
useSchemaToolbar,
SchemaSettingAccessControl,
+ useDataBlockProps,
+ useCollectionManager_deprecated,
} from '@nocobase/client';
import { CustomRequestSettingsItem } from './components/CustomRequestActionDesigner';
@@ -40,17 +42,11 @@ export const customizeCustomRequestActionSettings = new SchemaSettings({
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
- const { name } = useCollection() || {};
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
- collectionName: name,
};
},
- useVisible() {
- const record = useCollectionRecord();
- return record && record.data && !record?.isNew;
- },
},
{
name: 'secondConFirm',
diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts
index 8cb73835be..f22ce9d294 100644
--- a/packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts
+++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts
@@ -15,6 +15,17 @@ import Application from '@nocobase/server';
import axios from 'axios';
import CustomRequestPlugin from '../plugin';
+function toJSON(value) {
+ if (typeof value === 'string') {
+ try {
+ return JSON.parse(value);
+ } catch (error) {
+ return value;
+ }
+ }
+ return value;
+}
+
const getHeaders = (headers: Record) => {
return Object.keys(headers).reduce((hds, key) => {
if (key.toLocaleLowerCase().startsWith('x-')) {
@@ -73,6 +84,7 @@ export async function send(this: CustomRequestPlugin, ctx: Context, next: Next)
data: {},
},
$nForm,
+ $nSelectedRecord,
} = values;
// root role has all permissions
@@ -154,6 +166,7 @@ export async function send(this: CustomRequestPlugin, ctx: Context, next: Next)
$nToken: ctx.getBearerToken(),
$nForm,
$env: ctx.app.environment.getVariables(),
+ $nSelectedRecord,
};
const axiosRequestConfig = {
@@ -166,7 +179,7 @@ export async function send(this: CustomRequestPlugin, ctx: Context, next: Next)
...omitNullAndUndefined(getParsedValue(arrayToObject(headers), variables)),
},
params: getParsedValue(arrayToObject(params), variables),
- data: getParsedValue(data, variables),
+ data: getParsedValue(toJSON(data), variables),
};
const requestUrl = axios.getUri(axiosRequestConfig);
diff --git a/packages/plugins/@nocobase/plugin-action-duplicate/package.json b/packages/plugins/@nocobase/plugin-action-duplicate/package.json
index cd2d0c5238..2fce568739 100644
--- a/packages/plugins/@nocobase/plugin-action-duplicate/package.json
+++ b/packages/plugins/@nocobase/plugin-action-duplicate/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-action-duplicate",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-duplicate",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-duplicate",
diff --git a/packages/plugins/@nocobase/plugin-action-export/package.json b/packages/plugins/@nocobase/plugin-action-export/package.json
index 8bfd7c2ae9..98c4cefbad 100644
--- a/packages/plugins/@nocobase/plugin-action-export/package.json
+++ b/packages/plugins/@nocobase/plugin-action-export/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "操作:导出记录",
"description": "Export filtered records to excel, you can configure which fields to export.",
"description.zh-CN": "导出筛选后的记录到 Excel 中,可以配置导出哪些字段。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-export",
diff --git a/packages/plugins/@nocobase/plugin-action-export/src/client/schemaSettings.ts b/packages/plugins/@nocobase/plugin-action-export/src/client/schemaSettings.ts
index c1a066e16d..42fa2e46e8 100644
--- a/packages/plugins/@nocobase/plugin-action-export/src/client/schemaSettings.ts
+++ b/packages/plugins/@nocobase/plugin-action-export/src/client/schemaSettings.ts
@@ -9,7 +9,14 @@
import { ArrayItems } from '@formily/antd-v5';
import { ISchema, useField, useFieldSchema } from '@formily/react';
-import { ButtonEditor, SchemaSettings, useDesignable, useSchemaToolbar } from '@nocobase/client';
+import {
+ ButtonEditor,
+ SchemaSettings,
+ useDesignable,
+ useSchemaToolbar,
+ SchemaSettingsLinkageRules,
+ useDataBlockProps,
+} from '@nocobase/client';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useShared } from './useShared';
@@ -25,6 +32,16 @@ export const exportActionSchemaSettings = new SchemaSettings({
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'exportableFields',
type: 'actionModal',
@@ -65,6 +82,7 @@ export const exportActionSchemaSettings = new SchemaSettings({
};
},
},
+
{
name: 'divider',
type: 'divider',
diff --git a/packages/plugins/@nocobase/plugin-action-export/src/client/useExportAction.tsx b/packages/plugins/@nocobase/plugin-action-export/src/client/useExportAction.tsx
index c7c7ec8139..4bca136a76 100644
--- a/packages/plugins/@nocobase/plugin-action-export/src/client/useExportAction.tsx
+++ b/packages/plugins/@nocobase/plugin-action-export/src/client/useExportAction.tsx
@@ -37,7 +37,7 @@ export const useExportAction = () => {
const { name, title } = useCollection_deprecated();
const { t } = useExportTranslation();
const { modal } = App.useApp();
- const filters = service.params?.[1]?.filters || {};
+ const filters = service.params[0]?.filter || {};
const field = useField();
const exportLimit = useMemo(() => {
if (appInfo?.data?.exportLimit) {
@@ -81,7 +81,7 @@ export const useExportAction = () => {
{
title: compile(title),
appends: service.params[0]?.appends?.join(),
- filter: mergeFilter([...Object.values(filters), defaultFilter]),
+ filter: mergeFilter([filters, defaultFilter]),
sort: params?.sort,
values: {
columns: compile(exportSettings),
diff --git a/packages/plugins/@nocobase/plugin-action-import/package.json b/packages/plugins/@nocobase/plugin-action-import/package.json
index 05c6600bcd..7ef859058d 100644
--- a/packages/plugins/@nocobase/plugin-action-import/package.json
+++ b/packages/plugins/@nocobase/plugin-action-import/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "操作:导入记录",
"description": "Import records using excel templates. You can configure which fields to import and templates will be generated automatically.",
"description.zh-CN": "使用 Excel 模板导入数据,可以配置导入哪些字段,自动生成模板。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-import",
diff --git a/packages/plugins/@nocobase/plugin-action-import/src/client/schemaSettings.tsx b/packages/plugins/@nocobase/plugin-action-import/src/client/schemaSettings.tsx
index 968667acf0..78b7e7ad4f 100644
--- a/packages/plugins/@nocobase/plugin-action-import/src/client/schemaSettings.tsx
+++ b/packages/plugins/@nocobase/plugin-action-import/src/client/schemaSettings.tsx
@@ -9,14 +9,19 @@
import { ArrayItems } from '@formily/antd-v5';
import { ISchema, useField, useFieldSchema } from '@formily/react';
-import { ButtonEditor, SchemaSettings, type, useDesignable, useSchemaToolbar } from '@nocobase/client';
+import {
+ ButtonEditor,
+ SchemaSettings,
+ useDesignable,
+ useSchemaToolbar,
+ SchemaSettingsLinkageRules,
+} from '@nocobase/client';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useShared } from './useShared';
import { Button, Space } from 'antd';
import { Action } from '@nocobase/client';
import React from 'react';
-import { useDownloadXlsxTemplateAction } from './useImportAction';
export const importActionSchemaSettings = new SchemaSettings({
name: 'actionSettings:import',
@@ -29,6 +34,16 @@ export const importActionSchemaSettings = new SchemaSettings({
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'importableFields',
type: 'actionModal',
@@ -72,6 +87,7 @@ export const importActionSchemaSettings = new SchemaSettings({
};
},
},
+
{
name: 'divider',
type: 'divider',
diff --git a/packages/plugins/@nocobase/plugin-action-import/src/server/__tests__/xlsx-importer.test.ts b/packages/plugins/@nocobase/plugin-action-import/src/server/__tests__/xlsx-importer.test.ts
index 85cb90ca9a..21153d3760 100644
--- a/packages/plugins/@nocobase/plugin-action-import/src/server/__tests__/xlsx-importer.test.ts
+++ b/packages/plugins/@nocobase/plugin-action-import/src/server/__tests__/xlsx-importer.test.ts
@@ -2155,4 +2155,53 @@ describe('xlsx importer', () => {
expect(await Post.repository.count()).toBe(1);
});
+
+ it('should import time field successfully', async () => {
+ const TimeCollection = app.db.collection({
+ name: 'time_tests',
+ fields: [
+ {
+ type: 'time',
+ name: 'brithtime',
+ },
+ ],
+ });
+
+ await app.db.sync();
+ const templateCreator = new TemplateCreator({
+ collection: TimeCollection,
+ explain: 'test',
+ columns: [
+ {
+ dataIndex: ['birthtime'],
+ defaultTitle: '出生时间',
+ },
+ ],
+ });
+
+ const template = (await templateCreator.run({ returnXLSXWorkbook: true })) as XLSX.WorkBook;
+
+ const worksheet = template.Sheets[template.SheetNames[0]];
+
+ XLSX.utils.sheet_add_aoa(worksheet, [['12:12:12']], {
+ origin: 'A3',
+ });
+
+ const importer = new XlsxImporter({
+ collectionManager: app.mainDataSource.collectionManager,
+ collection: TimeCollection,
+ explain: 'test',
+ columns: [
+ {
+ dataIndex: ['brithtime'],
+ defaultTitle: '出生时间',
+ },
+ ],
+ workbook: template,
+ });
+
+ await importer.run();
+ const count = await TimeCollection.repository.count();
+ expect(count).toBe(1);
+ });
});
diff --git a/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts b/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts
index 8f8b75516f..9d63a78935 100644
--- a/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts
+++ b/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts
@@ -36,6 +36,7 @@ async function importXlsxAction(ctx: Context, next: Next) {
const workbook = XLSX.read(ctx.file.buffer, {
type: 'buffer',
sheetRows: readLimit,
+ cellDates: true,
});
const repository = ctx.getCurrentRepository() as Repository;
diff --git a/packages/plugins/@nocobase/plugin-action-print/package.json b/packages/plugins/@nocobase/plugin-action-print/package.json
index 1c9ac15fb3..bc6dfce4ca 100644
--- a/packages/plugins/@nocobase/plugin-action-print/package.json
+++ b/packages/plugins/@nocobase/plugin-action-print/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-action-print",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-print",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-print",
diff --git a/packages/plugins/@nocobase/plugin-action-print/src/client/PrintAction.Settings.tsx b/packages/plugins/@nocobase/plugin-action-print/src/client/PrintAction.Settings.tsx
index b27fa4a78b..d2051158a6 100644
--- a/packages/plugins/@nocobase/plugin-action-print/src/client/PrintAction.Settings.tsx
+++ b/packages/plugins/@nocobase/plugin-action-print/src/client/PrintAction.Settings.tsx
@@ -12,7 +12,6 @@ import {
SchemaSettings,
SchemaSettingsItemType,
SchemaSettingsLinkageRules,
- useCollection_deprecated,
useSchemaToolbar,
} from '@nocobase/client';
@@ -35,11 +34,9 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
- const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
- collectionName: name,
};
},
},
diff --git a/packages/plugins/@nocobase/plugin-ai/package.json b/packages/plugins/@nocobase/plugin-ai/package.json
index af19de2126..68ba33e54f 100644
--- a/packages/plugins/@nocobase/plugin-ai/package.json
+++ b/packages/plugins/@nocobase/plugin-ai/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "AI 集成",
"description": "Support integration with AI services, providing AI-related workflow nodes to enhance business processing capabilities.",
"description.zh-CN": "支持接入 AI 服务,提供 AI 相关的工作流节点,增强业务处理能力。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"peerDependencies": {
"@nocobase/client": "1.x",
diff --git a/packages/plugins/@nocobase/plugin-api-doc/package.json b/packages/plugins/@nocobase/plugin-api-doc/package.json
index cbe8653f5a..1885d5c275 100644
--- a/packages/plugins/@nocobase/plugin-api-doc/package.json
+++ b/packages/plugins/@nocobase/plugin-api-doc/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-api-doc",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "API documentation",
"displayName.zh-CN": "API 文档",
"description": "An OpenAPI documentation generator for NocoBase HTTP API.",
diff --git a/packages/plugins/@nocobase/plugin-api-keys/package.json b/packages/plugins/@nocobase/plugin-api-keys/package.json
index 2bb95250d0..248e051be4 100644
--- a/packages/plugins/@nocobase/plugin-api-keys/package.json
+++ b/packages/plugins/@nocobase/plugin-api-keys/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "认证:API 密钥",
"description": "Allows users to use API key to access application's HTTP API",
"description.zh-CN": "允许用户使用 API 密钥访问应用的 HTTP API",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/api-keys",
diff --git a/packages/plugins/@nocobase/plugin-async-task-manager/package.json b/packages/plugins/@nocobase/plugin-async-task-manager/package.json
index a7ae699cbf..185c71ea5e 100644
--- a/packages/plugins/@nocobase/plugin-async-task-manager/package.json
+++ b/packages/plugins/@nocobase/plugin-async-task-manager/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "异步任务管理器",
"description": "Manage and monitor asynchronous tasks such as data import/export. Support task progress tracking and notification.",
"description.zh-CN": "管理和监控数据导入导出等异步任务。支持任务进度跟踪和通知。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"peerDependencies": {
"@nocobase/client": "1.x",
diff --git a/packages/plugins/@nocobase/plugin-async-task-manager/src/client/AsyncTaskManagerProvider.tsx b/packages/plugins/@nocobase/plugin-async-task-manager/src/client/AsyncTaskManagerProvider.tsx
index 26fd598c84..d93ce6806d 100644
--- a/packages/plugins/@nocobase/plugin-async-task-manager/src/client/AsyncTaskManagerProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-async-task-manager/src/client/AsyncTaskManagerProvider.tsx
@@ -1,176 +1,24 @@
-import { PinnedPluginListProvider, SchemaComponentOptions, useApp } from '@nocobase/client';
+/**
+ * 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 { PinnedPluginListProvider, SchemaComponentOptions, useRequest } from '@nocobase/client';
+import React from 'react';
import { AsyncTasks } from './components/AsyncTasks';
-import React, { useEffect, useState, createContext, useContext, useCallback } from 'react';
-import { message } from 'antd';
-import { useT } from './locale';
-
-export const AsyncTaskContext = createContext(null);
-
-export const useAsyncTask = () => {
- const context = useContext(AsyncTaskContext);
- if (!context) {
- throw new Error('useAsyncTask must be used within AsyncTaskManagerProvider');
- }
- return context;
-};
export const AsyncTaskManagerProvider = (props) => {
- const app = useApp();
- const t = useT();
- const [tasks, setTasks] = useState([]);
- const [popoverVisible, setPopoverVisible] = useState(false);
- const [hasProcessingTasks, setHasProcessingTasks] = useState(false);
- const [cancellingTasks, setCancellingTasks] = useState>(new Set());
- const [modalVisible, setModalVisible] = useState(false);
- const [currentError, setCurrentError] = useState(null);
- const [resultModalVisible, setResultModalVisible] = useState(false);
- const [currentTask, setCurrentTask] = useState(null);
- const [wsAuthorized, setWsAuthorized] = useState(() => app.isWsAuthorized);
-
- useEffect(() => {
- setHasProcessingTasks(tasks.some((task) => task.status.type !== 'success' && task.status.type !== 'failed'));
- }, [tasks]);
-
- const handleTaskMessage = useCallback((event: CustomEvent) => {
- const tasks = event.detail;
- setTasks(tasks ? tasks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) : []);
- }, []);
-
- const handleTaskCreated = useCallback((event: CustomEvent) => {
- const taskData = event.detail;
- setTasks((prev) => {
- const newTasks = [taskData, ...prev];
- return newTasks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
- });
- setPopoverVisible(true);
- }, []);
-
- const handleTaskProgress = useCallback((event: CustomEvent) => {
- const { taskId, progress } = event.detail;
- setTasks((prev) => prev.map((task) => (task.taskId === taskId ? { ...task, progress } : task)));
- }, []);
-
- const handleTaskStatus = useCallback((event: CustomEvent) => {
- const { taskId, status } = event.detail;
- if (status.type === 'cancelled') {
- setTasks((prev) => prev.filter((task) => task.taskId !== taskId));
- } else {
- setTasks((prev) => {
- const newTasks = prev.map((task) => {
- if (task.taskId === taskId) {
- if (status.type === 'success' && task.status.type !== 'success') {
- message.success(t('Task completed'));
- }
- if (status.type === 'failed' && task.status.type !== 'failed') {
- message.error(t('Task failed'));
- }
- return { ...task, status };
- }
- return task;
- });
- return newTasks;
- });
- }
- }, []);
-
- const handleWsAuthorized = useCallback(() => {
- setWsAuthorized(true);
- }, []);
-
- const handleTaskCancelled = useCallback((event: CustomEvent) => {
- const { taskId } = event.detail;
- setCancellingTasks((prev) => {
- const newSet = new Set(prev);
- newSet.delete(taskId);
- return newSet;
- });
- message.success(t('Task cancelled'));
- }, []);
-
- useEffect(() => {
- app.eventBus.addEventListener('ws:message:async-tasks', handleTaskMessage);
- app.eventBus.addEventListener('ws:message:async-tasks:created', handleTaskCreated);
- app.eventBus.addEventListener('ws:message:async-tasks:progress', handleTaskProgress);
- app.eventBus.addEventListener('ws:message:async-tasks:status', handleTaskStatus);
- app.eventBus.addEventListener('ws:message:authorized', handleWsAuthorized);
- app.eventBus.addEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
-
- if (wsAuthorized) {
- app.ws.send(
- JSON.stringify({
- type: 'request:async-tasks:list',
- }),
- );
- }
-
- return () => {
- app.eventBus.removeEventListener('ws:message:async-tasks', handleTaskMessage);
- app.eventBus.removeEventListener('ws:message:async-tasks:created', handleTaskCreated);
- app.eventBus.removeEventListener('ws:message:async-tasks:progress', handleTaskProgress);
- app.eventBus.removeEventListener('ws:message:async-tasks:status', handleTaskStatus);
- app.eventBus.removeEventListener('ws:message:authorized', handleWsAuthorized);
- app.eventBus.removeEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
- };
- }, [
- app,
- handleTaskMessage,
- handleTaskCreated,
- handleTaskProgress,
- handleTaskStatus,
- handleWsAuthorized,
- handleTaskCancelled,
- wsAuthorized,
- ]);
-
- const handleCancelTask = async (taskId: string) => {
- setCancellingTasks((prev) => new Set(prev).add(taskId));
- try {
- app.ws.send(
- JSON.stringify({
- type: 'request:async-tasks:cancel',
- payload: { taskId },
- }),
- );
- } catch (error) {
- console.error('Failed to cancel task:', error);
- setCancellingTasks((prev) => {
- const newSet = new Set(prev);
- newSet.delete(taskId);
- return newSet;
- });
- }
- };
-
- const contextValue = {
- tasks,
- popoverVisible,
- setPopoverVisible,
- hasProcessingTasks,
- cancellingTasks,
- modalVisible,
- setModalVisible,
- currentError,
- setCurrentError,
- resultModalVisible,
- setResultModalVisible,
- currentTask,
- setCurrentTask,
- handleCancelTask,
- };
-
return (
-
- 0
- ? {
- asyncTasks: { order: 300, component: 'AsyncTasks', pin: true, snippet: '*' },
- }
- : {}
- }
- >
- {props.children}
-
-
+
+ {props.children}
+
);
};
diff --git a/packages/plugins/@nocobase/plugin-async-task-manager/src/client/components/AsyncTasks.tsx b/packages/plugins/@nocobase/plugin-async-task-manager/src/client/components/AsyncTasks.tsx
index d53b50f760..af2d5e19be 100644
--- a/packages/plugins/@nocobase/plugin-async-task-manager/src/client/components/AsyncTasks.tsx
+++ b/packages/plugins/@nocobase/plugin-async-task-manager/src/client/components/AsyncTasks.tsx
@@ -1,13 +1,31 @@
-import React, { useEffect } from 'react';
-import { Button, Popover, Table, Tag, Progress, Space, Tooltip, Popconfirm, Modal, Empty } from 'antd';
-import { createStyles, Icon, useApp, usePlugin } from '@nocobase/client';
+/**
+ * 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 {
+ createStyles,
+ Icon,
+ useAPIClient,
+ useApp,
+ usePlugin,
+ useRequest,
+ useCollectionManager,
+ useCompile,
+} from '@nocobase/client';
+import { Button, Empty, Modal, Popconfirm, Popover, Progress, Space, Table, Tag, Tooltip } from 'antd';
+import React, { useCallback, useEffect, useState } from 'react';
+
+import { useCurrentAppInfo } from '@nocobase/client';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useT } from '../locale';
-import { useAsyncTask } from '../AsyncTaskManagerProvider';
-import { useCurrentAppInfo } from '@nocobase/client';
+
const useStyles = createStyles(({ token }) => {
return {
button: {
@@ -34,93 +52,27 @@ const renderTaskResult = (status, t) => {
);
};
-export const AsyncTasks = () => {
- const {
- tasks,
- popoverVisible,
- setPopoverVisible,
- hasProcessingTasks,
- cancellingTasks,
- modalVisible,
- setModalVisible,
- currentError,
- setCurrentError,
- resultModalVisible,
- setResultModalVisible,
- currentTask,
- setCurrentTask,
- handleCancelTask,
- } = useAsyncTask();
+const useAsyncTask = () => {
+ const { data, refreshAsync, loading } = useRequest({
+ url: 'asyncTasks:list',
+ });
+ return { loading, tasks: data?.data || [], refresh: refreshAsync };
+};
- const plugin = usePlugin('async-task-manager');
+const AsyncTasksButton = (props) => {
+ const { popoverVisible, setPopoverVisible, tasks, refresh, loading, hasProcessingTasks } = props;
const app = useApp();
+ const api = useAPIClient();
const appInfo = useCurrentAppInfo();
const t = useT();
const { styles } = useStyles();
-
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (popoverVisible) {
- const popoverElements = document.querySelectorAll('.ant-popover');
- const buttonElement = document.querySelector('.sync-task-button');
- let clickedInside = false;
-
- popoverElements.forEach((element) => {
- if (element.contains(event.target as Node)) {
- clickedInside = true;
- }
- });
-
- if (buttonElement?.contains(event.target as Node)) {
- clickedInside = true;
- }
-
- if (!clickedInside) {
- setPopoverVisible(false);
- }
- }
- };
-
- document.addEventListener('click', handleClickOutside);
- return () => {
- document.removeEventListener('click', handleClickOutside);
- };
- }, [popoverVisible, setPopoverVisible]);
-
+ const plugin = usePlugin('async-task-manager');
+ const cm = useCollectionManager();
+ const compile = useCompile();
const showTaskResult = (task) => {
- setCurrentTask(task);
- setResultModalVisible(true);
setPopoverVisible(false);
};
- const renderTaskResultModal = () => {
- if (!currentTask) {
- return;
- }
-
- const { payload } = currentTask.status;
- const renderer = plugin.taskResultRendererManager.get(currentTask.title.actionType);
-
- return (
- setResultModalVisible(false)}>
- {t('Close')}
- ,
- ]}
- onCancel={() => setResultModalVisible(false)}
- >
- {renderer ? (
- React.createElement(renderer, { payload, task: currentTask })
- ) : (
- {t(`No renderer available for this task type, payload: ${payload}`)}
- )}
-
- );
- };
-
const columns = [
{
title: t('Created at'),
@@ -140,7 +92,7 @@ export const AsyncTasks = () => {
if (!title) {
return '-';
}
-
+ const collection = cm.getCollection(title.collection);
const actionTypeMap = {
export: t('Export'),
import: t('Import'),
@@ -156,7 +108,7 @@ export const AsyncTasks = () => {
};
const taskTemplate = taskTypeMap[title.actionType] || `${actionText}`;
- return taskTemplate.replace('{collection}', title.collection);
+ return taskTemplate.replace('{collection}', compile(collection?.title || title.collection));
},
},
{
@@ -274,7 +226,7 @@ export const AsyncTasks = () => {
width: 180,
render: (_, record: any) => {
const actions = [];
- const isTaskCancelling = cancellingTasks.has(record.taskId);
+ const isTaskCancelling = false;
if ((record.status.type === 'running' || record.status.type === 'pending') && record.cancelable) {
actions.push(
@@ -282,7 +234,15 @@ export const AsyncTasks = () => {
key="cancel"
title={t('Confirm cancel')}
description={t('Confirm cancel description')}
- onConfirm={() => handleCancelTask(record.taskId)}
+ onConfirm={async () => {
+ await api.request({
+ url: 'asyncTasks:cancel',
+ params: {
+ filterByTk: record.taskId,
+ },
+ });
+ refresh();
+ }}
okText={t('Confirm')}
cancelText={t('Cancel')}
disabled={isTaskCancelling}
@@ -309,8 +269,16 @@ export const AsyncTasks = () => {
icon={ }
onClick={() => {
const token = app.apiClient.auth.token;
+ const collection = cm.getCollection(record.title.collection);
+ const compiledTitle = compile(collection?.title);
+ const suffix = record?.title?.actionType === 'export-attachments' ? '-attachments.zip' : '.xlsx';
+ const fileText = `${compiledTitle}${suffix}`;
+ const filename =
+ record?.title?.actionType !== 'create migration' ? encodeURIComponent(fileText) : null;
const url = app.getApiUrl(
- `asyncTasks:fetchFile/${record.taskId}?token=${token}&__appName=${appInfo?.data?.name || app.name}`,
+ `asyncTasks:fetchFile/${record.taskId}?token=${token}&__appName=${encodeURIComponent(
+ appInfo?.data?.name || app.name,
+ )}${filename ? `&filename=${filename}` : ''}`,
);
window.open(url);
}}
@@ -325,7 +293,19 @@ export const AsyncTasks = () => {
type="link"
size="small"
icon={ }
- onClick={() => showTaskResult(record)}
+ onClick={() => {
+ showTaskResult(record);
+ const { payload } = record.status;
+ const renderer = plugin.taskResultRendererManager.get(record.title.actionType);
+ Modal.info({
+ title: t('Task result'),
+ content: renderer ? (
+ React.createElement(renderer, { payload, task: record })
+ ) : (
+ {t(`No renderer available for this task type, payload: ${payload}`)}
+ ),
+ });
+ }}
>
{t('View result')}
,
@@ -341,9 +321,22 @@ export const AsyncTasks = () => {
size="small"
icon={ }
onClick={() => {
- setCurrentError(record.status.errors);
- setModalVisible(true);
setPopoverVisible(false);
+ Modal.info({
+ title: t('Error Details'),
+ content: record.status.errors?.map((error, index) => (
+
+
{error.message}
+ {error.code && (
+
+ {t('Error code')}: {error.code}
+
+ )}
+
+ )),
+ closable: true,
+ width: 400,
+ });
}}
>
{t('Error details')}
@@ -357,9 +350,9 @@ export const AsyncTasks = () => {
];
const content = (
- 0 ? 800 : 200 }}>
+
0 ? 800 : 200 }}>
{tasks.length > 0 ? (
-
+
) : (
@@ -383,30 +376,53 @@ export const AsyncTasks = () => {
onClick={() => setPopoverVisible(!popoverVisible)}
/>
- {renderTaskResultModal()}
-
-
setModalVisible(false)}
- footer={[
- setModalVisible(false)}>
- {t('OK')}
- ,
- ]}
- width={400}
- >
- {currentError?.map((error, index) => (
-
-
{error.message}
- {error.code && (
-
- {t('Error code')}: {error.code}
-
- )}
-
- ))}
-
>
);
};
+
+export const AsyncTasks = () => {
+ const { tasks, refresh, ...others } = useAsyncTask();
+ const app = useApp();
+ const [popoverVisible, setPopoverVisible] = useState(false);
+ const [hasProcessingTasks, setHasProcessingTasks] = useState(false);
+
+ useEffect(() => {
+ setHasProcessingTasks(tasks.some((task) => task.status.type !== 'success' && task.status.type !== 'failed'));
+ }, [tasks]);
+
+ const handleTaskCreated = useCallback(async () => {
+ setPopoverVisible(true);
+ }, []);
+ const handleTaskProgress = useCallback(() => {
+ refresh();
+ console.log('handleTaskProgress');
+ }, []);
+ const handleTaskStatus = useCallback(() => {
+ refresh();
+ console.log('handleTaskStatus');
+ }, []);
+ const handleTaskCancelled = useCallback(() => {
+ refresh();
+ console.log('handleTaskCancelled');
+ }, []);
+
+ useEffect(() => {
+ app.eventBus.addEventListener('ws:message:async-tasks:created', handleTaskCreated);
+ app.eventBus.addEventListener('ws:message:async-tasks:progress', handleTaskProgress);
+ app.eventBus.addEventListener('ws:message:async-tasks:status', handleTaskStatus);
+ app.eventBus.addEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
+
+ return () => {
+ app.eventBus.removeEventListener('ws:message:async-tasks:created', handleTaskCreated);
+ app.eventBus.removeEventListener('ws:message:async-tasks:progress', handleTaskProgress);
+ app.eventBus.removeEventListener('ws:message:async-tasks:status', handleTaskStatus);
+ app.eventBus.removeEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
+ };
+ }, [app, handleTaskCancelled, handleTaskCreated, handleTaskProgress, handleTaskStatus]);
+
+ return (
+ tasks?.length > 0 && (
+
+ )
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-async-task-manager/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-async-task-manager/src/server/plugin.ts
index 494d826848..76fdb0ac1d 100644
--- a/packages/plugins/@nocobase/plugin-async-task-manager/src/server/plugin.ts
+++ b/packages/plugins/@nocobase/plugin-async-task-manager/src/server/plugin.ts
@@ -1,9 +1,18 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
import { Plugin } from '@nocobase/server';
-import { BaseTaskManager } from './base-task-manager';
-import { AsyncTasksManager } from './interfaces/async-task-manager';
-import { CommandTaskType } from './command-task-type';
-import asyncTasksResource from './resourcers/async-tasks';
import { throttle } from 'lodash';
+import { BaseTaskManager } from './base-task-manager';
+import { CommandTaskType } from './command-task-type';
+import { AsyncTasksManager } from './interfaces/async-task-manager';
+import asyncTasksResource from './resourcers/async-tasks';
export class PluginAsyncExportServer extends Plugin {
private progressThrottles: Map
= new Map();
@@ -20,7 +29,7 @@ export class PluginAsyncExportServer extends Plugin {
});
this.app.container.get('AsyncTaskManager').registerTaskType(CommandTaskType);
- this.app.acl.allow('asyncTasks', ['get', 'fetchFile'], 'loggedIn');
+ this.app.acl.allow('asyncTasks', ['list', 'get', 'fetchFile', 'cancel'], 'loggedIn');
}
getThrottledProgressEmitter(taskId: string, userId: string) {
diff --git a/packages/plugins/@nocobase/plugin-async-task-manager/src/server/resourcers/async-tasks.ts b/packages/plugins/@nocobase/plugin-async-task-manager/src/server/resourcers/async-tasks.ts
index 1733616ae3..31bece6dfe 100644
--- a/packages/plugins/@nocobase/plugin-async-task-manager/src/server/resourcers/async-tasks.ts
+++ b/packages/plugins/@nocobase/plugin-async-task-manager/src/server/resourcers/async-tasks.ts
@@ -1,8 +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.
+ */
+
import fs from 'fs';
+import _ from 'lodash';
import { basename } from 'path';
+
export default {
name: 'asyncTasks',
actions: {
+ async list(ctx, next) {
+ const userId = ctx.auth.user.id;
+ const asyncTaskManager = ctx.app.container.get('AsyncTaskManager');
+ const tasks = await asyncTaskManager.getTasksByTag('userId', userId);
+ ctx.body = _.orderBy(tasks, 'createdAt', 'desc');
+ await next();
+ },
async get(ctx, next) {
const { filterByTk } = ctx.action.params;
const taskManager = ctx.app.container.get('AsyncTaskManager');
@@ -11,8 +29,29 @@ export default {
ctx.body = taskStatus;
await next();
},
- async fetchFile(ctx, next) {
+ async cancel(ctx, next) {
const { filterByTk } = ctx.action.params;
+ const userId = ctx.auth.user.id;
+ const asyncTaskManager = ctx.app.container.get('AsyncTaskManager');
+
+ const task = asyncTaskManager.getTask(filterByTk);
+
+ if (!task) {
+ ctx.body = 'ok';
+ await next();
+ return;
+ }
+
+ if (task.tags['userId'] != userId) {
+ ctx.throw(403);
+ }
+
+ const cancelled = await asyncTaskManager.cancelTask(filterByTk);
+ ctx.body = cancelled;
+ await next();
+ },
+ async fetchFile(ctx, next) {
+ const { filterByTk, filename } = ctx.action.params;
const taskManager = ctx.app.container.get('AsyncTaskManager');
const taskStatus = await taskManager.getTaskStatus(filterByTk);
// throw error if task is not success
@@ -28,10 +67,12 @@ export default {
// send file to client
ctx.body = fs.createReadStream(filePath);
-
+ // 处理文件名
+ let finalFileName = filename ? filename : basename(filePath);
+ finalFileName = encodeURIComponent(finalFileName); // 避免中文或特殊字符问题
ctx.set({
'Content-Type': 'application/octet-stream',
- 'Content-Disposition': `attachment; filename=${basename(filePath)}`,
+ 'Content-Disposition': `attachment; filename=${finalFileName}`,
});
await next();
diff --git a/packages/plugins/@nocobase/plugin-audit-logs/package.json b/packages/plugins/@nocobase/plugin-audit-logs/package.json
index 13e4a17368..03d19886e6 100644
--- a/packages/plugins/@nocobase/plugin-audit-logs/package.json
+++ b/packages/plugins/@nocobase/plugin-audit-logs/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-audit-logs",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "Audit logs (deprecated)",
"displayName.zh-CN": "审计日志(废弃)",
"description": "This plugin is deprecated. There will be a new audit log plugin in the future.",
diff --git a/packages/plugins/@nocobase/plugin-auth-sms/package.json b/packages/plugins/@nocobase/plugin-auth-sms/package.json
index 78dd27b9d8..4155dd1fcb 100644
--- a/packages/plugins/@nocobase/plugin-auth-sms/package.json
+++ b/packages/plugins/@nocobase/plugin-auth-sms/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "认证:短信",
"description": "SMS authentication.",
"description.zh-CN": "通过短信验证码认证身份。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/auth-sms",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth-sms",
diff --git a/packages/plugins/@nocobase/plugin-auth/package.json b/packages/plugins/@nocobase/plugin-auth/package.json
index 65d9ac9717..24ff85f6ec 100644
--- a/packages/plugins/@nocobase/plugin-auth/package.json
+++ b/packages/plugins/@nocobase/plugin-auth/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-auth",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/auth",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth",
diff --git a/packages/plugins/@nocobase/plugin-auth/src/client/AuthProvider.tsx b/packages/plugins/@nocobase/plugin-auth/src/client/AuthProvider.tsx
index 9f88e94e0e..85ab4b286c 100644
--- a/packages/plugins/@nocobase/plugin-auth/src/client/AuthProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-auth/src/client/AuthProvider.tsx
@@ -13,15 +13,13 @@ import React, { useEffect } from 'react';
export const AuthProvider: React.FC = (props) => {
const searchString = useLocationSearch();
const app = useApp();
+ const params = new URLSearchParams(searchString);
+ const authenticator = params.get('authenticator');
+ const token = params.get('token');
+ if (token) {
+ app.apiClient.auth.setToken(token);
+ app.apiClient.auth.setAuthenticator(authenticator);
+ }
- useEffect(() => {
- const params = new URLSearchParams(searchString);
- const authenticator = params.get('authenticator');
- const token = params.get('token');
- if (token) {
- app.apiClient.auth.setToken(token);
- app.apiClient.auth.setAuthenticator(authenticator);
- }
- });
return <>{props.children}>;
};
diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/basic-auth.ts b/packages/plugins/@nocobase/plugin-auth/src/server/basic-auth.ts
index 5a378e9ddd..2b92203cef 100644
--- a/packages/plugins/@nocobase/plugin-auth/src/server/basic-auth.ts
+++ b/packages/plugins/@nocobase/plugin-auth/src/server/basic-auth.ts
@@ -47,7 +47,7 @@ export class BasicAuth extends BaseAuth {
const valid = await field.verify(password, user.password);
if (!valid) {
ctx.throw(401, ctx.t('The username/email or password is incorrect, please re-enter', { ns: namespace }), {
- code: 'INCORRECT_PASSWORD',
+ internalCode: 'INCORRECT_PASSWORD',
user,
});
}
diff --git a/packages/plugins/@nocobase/plugin-backup-restore/package.json b/packages/plugins/@nocobase/plugin-backup-restore/package.json
index e0e23c61f8..050b0f31f4 100644
--- a/packages/plugins/@nocobase/plugin-backup-restore/package.json
+++ b/packages/plugins/@nocobase/plugin-backup-restore/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "应用的备份与还原(废弃)",
"description": "Backup and restore applications for scenarios such as application replication, migration, and upgrades.",
"description.zh-CN": "备份和还原应用,可用于应用的复制、迁移、升级等场景。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/backup-restore",
@@ -27,8 +27,8 @@
"mkdirp": "^1.0.4",
"object-path": "^0.11.8",
"react": "^18.2.0",
- "semver": "^7.5.4",
- "tar": "^6.1.13"
+ "semver": "^7.7.1",
+ "tar": "^7.4.3"
},
"peerDependencies": {
"@nocobase/actions": "1.x",
diff --git a/packages/plugins/@nocobase/plugin-block-iframe/package.json b/packages/plugins/@nocobase/plugin-block-iframe/package.json
index 21a43b7c21..64268369a4 100644
--- a/packages/plugins/@nocobase/plugin-block-iframe/package.json
+++ b/packages/plugins/@nocobase/plugin-block-iframe/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "区块:iframe",
"description": "Create an iframe block on the page to embed and display external web pages or content.",
"description.zh-CN": "在页面上创建和管理iframe,用于嵌入和展示外部网页或内容。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/block-iframe",
diff --git a/packages/plugins/@nocobase/plugin-block-iframe/src/client/Iframe.tsx b/packages/plugins/@nocobase/plugin-block-iframe/src/client/Iframe.tsx
index 33394867d1..3c04d26b1e 100644
--- a/packages/plugins/@nocobase/plugin-block-iframe/src/client/Iframe.tsx
+++ b/packages/plugins/@nocobase/plugin-block-iframe/src/client/Iframe.tsx
@@ -17,7 +17,7 @@ import {
useRequest,
useVariables,
} from '@nocobase/client';
-import { Card, Spin } from 'antd';
+import { Card, Spin, theme } from 'antd';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import RIframe from 'react-iframe';
@@ -37,6 +37,7 @@ export const Iframe: any = observer(
const { url, htmlId, mode = 'url', height, html, params, engine, ...others } = props;
const field = useField();
const { t } = useTranslation();
+ const { token } = theme.useToken();
const targetHeight = useBlockHeight() || height;
const variables = useVariables();
const localVariables = useLocalVariables();
@@ -90,7 +91,9 @@ export const Iframe: any = observer(
}, [htmlContent, mode, url, variables, localVariables, params]);
if ((mode === 'url' && !url) || (mode === 'html' && !htmlId)) {
return (
-
+
{t('Please fill in the iframe URL')}
);
@@ -101,7 +104,7 @@ export const Iframe: any = observer(
diff --git a/packages/plugins/@nocobase/plugin-block-template/package.json b/packages/plugins/@nocobase/plugin-block-template/package.json
index cfffc11745..51837bc095 100644
--- a/packages/plugins/@nocobase/plugin-block-template/package.json
+++ b/packages/plugins/@nocobase/plugin-block-template/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "区块:模板",
"description": "Create and manage block templates for reuse on pages.",
"description.zh-CN": "创建和管理区块模板,用于在页面中重复使用。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/block-template",
diff --git a/packages/plugins/@nocobase/plugin-block-template/src/client/components/BlockTemplatePage.tsx b/packages/plugins/@nocobase/plugin-block-template/src/client/components/BlockTemplatePage.tsx
index 8bde584a6f..7fc01839b4 100644
--- a/packages/plugins/@nocobase/plugin-block-template/src/client/components/BlockTemplatePage.tsx
+++ b/packages/plugins/@nocobase/plugin-block-template/src/client/components/BlockTemplatePage.tsx
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { useRequest, RemoteSchemaComponent } from '@nocobase/client';
+import { useRequest, RemoteSchemaComponent, AllDataBlocksProvider } from '@nocobase/client';
import React from 'react';
import { useT } from '../locale';
import { useParams } from 'react-router';
@@ -31,12 +31,15 @@ export const BlockTemplatePage = () => {
const schemaUid = data?.data?.uid;
return (
-
+
{
]}
/>
-
+
);
};
diff --git a/packages/plugins/@nocobase/plugin-block-template/src/client/components/SaveAsTemplateSetting.tsx b/packages/plugins/@nocobase/plugin-block-template/src/client/components/SaveAsTemplateSetting.tsx
index 3e50a94d78..1ab2d6a7a5 100644
--- a/packages/plugins/@nocobase/plugin-block-template/src/client/components/SaveAsTemplateSetting.tsx
+++ b/packages/plugins/@nocobase/plugin-block-template/src/client/components/SaveAsTemplateSetting.tsx
@@ -7,7 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { ISchema, SchemaSettingsModalItem, useResource, useSchemaSettings } from '@nocobase/client';
+import {
+ ISchema,
+ SchemaSettingsModalItem,
+ useResource,
+ useSchemaSettings,
+ useSchemaTemplateManager,
+} from '@nocobase/client';
import React from 'react';
import { useT } from '../locale';
import { useFieldSchema, useField, useForm } from '@formily/react';
@@ -48,6 +54,7 @@ export const SaveAsTemplateSetting = () => {
const { templates } = useBlockTemplateMenus();
const location = useLocation();
const { template: deprecatedTemplate } = useSchemaSettings();
+ const schemaTemplateManager = useSchemaTemplateManager();
return (
{
const schemaUid = uid();
const isMobile = type === 'Mobile';
const templateSchema = getTemplateSchemaFromPage(fieldSchema.toJSON());
- if (deprecatedTemplate || containsReferenceTemplate(templateSchema)) {
+ if (deprecatedTemplate || (await containsReferenceTemplate(templateSchema, schemaTemplateManager))) {
message.error(t('This block is using some reference templates, please convert to duplicate template first.'));
return;
}
@@ -298,13 +305,19 @@ function getTemplateSchemaFromPage(schema: ISchema) {
return templateSchema;
}
-function containsReferenceTemplate(schema: ISchema) {
+async function containsReferenceTemplate(
+ schema: ISchema,
+ schemaTemplateManager: ReturnType,
+) {
if (schema['x-component'] === 'BlockTemplate') {
- return true;
+ const templateId = schema['x-component-props']?.templateId;
+ if (templateId && schemaTemplateManager.getTemplateById(templateId)) {
+ return true;
+ }
}
if (schema.properties) {
for (const key in schema.properties) {
- if (containsReferenceTemplate(schema.properties[key])) {
+ if (await containsReferenceTemplate(schema.properties[key], schemaTemplateManager)) {
return true;
}
}
diff --git a/packages/plugins/@nocobase/plugin-block-template/src/client/utils/template.ts b/packages/plugins/@nocobase/plugin-block-template/src/client/utils/template.ts
index a7b7b88873..f32009378a 100644
--- a/packages/plugins/@nocobase/plugin-block-template/src/client/utils/template.ts
+++ b/packages/plugins/@nocobase/plugin-block-template/src/client/utils/template.ts
@@ -180,14 +180,14 @@ function shouldDeleteNoComponentSchema(schema: ISchema) {
return true;
}
const properties = schema?.properties;
- return properties && Object.values(properties).some((s) => s['x-component'] === undefined);
+ return properties && Object.values(properties).some((s) => s['x-component'] == null);
}
function cleanSchema(schema?: any) {
const properties = schema?.properties || {};
for (const key of Object.keys(properties)) {
- // 如果x-component是undefined
- if (schema.properties[key]['x-component'] === undefined && shouldDeleteNoComponentSchema(schema.properties[key])) {
+ // 如果x-component是undefined/null
+ if (schema.properties[key]['x-component'] == null && shouldDeleteNoComponentSchema(schema.properties[key])) {
delete schema.properties[key];
}
// 如果x-component是Grid.Row,且内部无任何内容,则删除
@@ -337,7 +337,7 @@ export function getFullSchema(
for (const key in schema.properties) {
const property = schema.properties[key];
schema.properties[key] = getFullSchema(property, templateschemacache, templateInfos, savedSchemaUids);
- if (schema.properties[key]['x-component'] === undefined) {
+ if (schema.properties[key]['x-component'] == null) {
delete schema.properties[key]; // 说明已经从模板中删除了
}
}
diff --git a/packages/plugins/@nocobase/plugin-block-template/src/server/collections/blockTemplateLinks.ts b/packages/plugins/@nocobase/plugin-block-template/src/server/collections/blockTemplateLinks.ts
index 7288070417..bd31cdea82 100644
--- a/packages/plugins/@nocobase/plugin-block-template/src/server/collections/blockTemplateLinks.ts
+++ b/packages/plugins/@nocobase/plugin-block-template/src/server/collections/blockTemplateLinks.ts
@@ -1,8 +1,18 @@
+/**
+ * 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 { defineCollection } from '@nocobase/database';
export default defineCollection({
dumpRules: 'required',
name: 'blockTemplateLinks',
+ migrationRules: ['overwrite', 'schema-only'],
fields: [
{
type: 'string',
diff --git a/packages/plugins/@nocobase/plugin-block-template/src/server/collections/blockTemplates.ts b/packages/plugins/@nocobase/plugin-block-template/src/server/collections/blockTemplates.ts
index 29ef81780a..38b0fe74c9 100644
--- a/packages/plugins/@nocobase/plugin-block-template/src/server/collections/blockTemplates.ts
+++ b/packages/plugins/@nocobase/plugin-block-template/src/server/collections/blockTemplates.ts
@@ -13,6 +13,7 @@ export default defineCollection({
dumpRules: 'required',
name: 'blockTemplates',
autoGenId: false,
+ migrationRules: ['overwrite', 'schema-only'],
fields: [
{
type: 'uid',
diff --git a/packages/plugins/@nocobase/plugin-block-template/src/server/utils/template.ts b/packages/plugins/@nocobase/plugin-block-template/src/server/utils/template.ts
index e93f6a62f6..f4e16061ce 100644
--- a/packages/plugins/@nocobase/plugin-block-template/src/server/utils/template.ts
+++ b/packages/plugins/@nocobase/plugin-block-template/src/server/utils/template.ts
@@ -376,7 +376,7 @@ function shouldDeleteNoComponentSchema(schema: Schema) {
return true;
}
const properties = schema?.properties;
- return properties && Object.values(properties).some((s) => s['x-component'] === undefined);
+ return properties && Object.values(properties).some((s) => s['x-component'] == null);
}
export function cleanSchema(schema?: Schema, templateId?: string) {
@@ -390,7 +390,7 @@ export function cleanSchema(schema?: Schema, templateId?: string) {
}
for (const key of Object.keys(properties)) {
if (
- schema.properties[key]['x-component'] === undefined &&
+ schema.properties[key]['x-component'] == null &&
!schema.properties[key]['x-template-root-uid'] &&
shouldDeleteNoComponentSchema(schema.properties[key])
) {
diff --git a/packages/plugins/@nocobase/plugin-block-workbench/package.json b/packages/plugins/@nocobase/plugin-block-workbench/package.json
index d7b91dc4e9..436343e691 100644
--- a/packages/plugins/@nocobase/plugin-block-workbench/package.json
+++ b/packages/plugins/@nocobase/plugin-block-workbench/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-block-workbench",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "Block: Action panel",
"displayName.zh-CN": "区块:操作面板",
"description": "Centrally manages and displays various actions, allowing users to efficiently perform tasks. It supports extensibility, with current action types including pop-ups, links, scanning, and custom requests.",
diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx
index b82a04c872..56814c760d 100644
--- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx
+++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx
@@ -8,10 +8,18 @@
*/
import { useFieldSchema } from '@formily/react';
-import { Action, Icon, useCompile, useComponent, withDynamicSchemaProps, ACLActionProvider } from '@nocobase/client';
+import {
+ Action,
+ Icon,
+ useComponent,
+ withDynamicSchemaProps,
+ ACLActionProvider,
+ NAMESPACE_UI_SCHEMA,
+} from '@nocobase/client';
import { Avatar } from 'antd';
import { createStyles } from 'antd-style';
import React, { useContext } from 'react';
+import { useTranslation } from 'react-i18next';
import { WorkbenchBlockContext } from './WorkbenchBlock';
import { WorkbenchLayout } from './workbenchBlockSettings';
@@ -40,8 +48,8 @@ function Button() {
const backgroundColor = fieldSchema['x-component-props']?.['iconColor'];
const { layout, ellipsis = true } = useContext(WorkbenchBlockContext);
const { styles, cx } = useStyles();
- const compile = useCompile();
- const title = compile(fieldSchema.title);
+ const { t } = useTranslation();
+ const title = t(fieldSchema.title, { ns: NAMESPACE_UI_SCHEMA });
return layout === WorkbenchLayout.Grid ? (
} />
diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx
index 833cce7a8e..af444909af 100644
--- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx
+++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx
@@ -14,6 +14,8 @@ import {
useSchemaInitializer,
ModalActionSchemaInitializerItem,
SchemaSettingAccessControl,
+ SchemaSettingsLinkageRules,
+ useSchemaToolbar,
} from '@nocobase/client';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -27,6 +29,16 @@ export const workbenchActionSettingsCustomRequest = new SchemaSettings({
return { hasIconColor: true };
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'editLink',
Component: SchemaSettingsActionLinkItem,
diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchLinkActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchLinkActionSchemaInitializerItem.tsx
index 7b17a862f6..fb91d8aa3f 100644
--- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchLinkActionSchemaInitializerItem.tsx
+++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchLinkActionSchemaInitializerItem.tsx
@@ -15,6 +15,8 @@ import {
useSchemaInitializerItem,
ModalActionSchemaInitializerItem,
SchemaSettingAccessControl,
+ SchemaSettingsLinkageRules,
+ useSchemaToolbar,
} from '@nocobase/client';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -29,6 +31,16 @@ export const workbenchActionSettingsLink = new SchemaSettings({
return { hasIconColor: true };
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'editLink',
Component: SchemaSettingsActionLinkItem,
diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx
index 3d1e7b09c5..d2f95e0f4d 100644
--- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx
+++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx
@@ -15,6 +15,8 @@ import {
useOpenModeContext,
ModalActionSchemaInitializerItem,
SchemaSettingAccessControl,
+ SchemaSettingsLinkageRules,
+ useSchemaToolbar,
} from '@nocobase/client';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -29,7 +31,16 @@ export const workbenchActionSettingsPopup = new SchemaSettings({
return { hasIconColor: true };
},
},
-
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'openMode',
Component: SchemaSettingOpenModeSchemaItems,
diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchScanActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchScanActionSchemaInitializerItem.tsx
index 47f2c87587..16a477bcd6 100644
--- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchScanActionSchemaInitializerItem.tsx
+++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchScanActionSchemaInitializerItem.tsx
@@ -15,6 +15,8 @@ import {
useSchemaInitializerItem,
ModalActionSchemaInitializerItem,
SchemaSettingAccessControl,
+ SchemaSettingsLinkageRules,
+ useSchemaToolbar,
} from '@nocobase/client';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -29,6 +31,16 @@ export const workbenchActionSettingsScanQrCode = new SchemaSettings({
return { hasIconColor: true };
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
...SchemaSettingAccessControl,
useVisible() {
diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/index.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/index.tsx
index 15c46eb4ec..9365c8ee67 100644
--- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/index.tsx
+++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/index.tsx
@@ -9,13 +9,13 @@
import { FileImageOutlined, LeftOutlined } from '@ant-design/icons';
import { useActionContext } from '@nocobase/client';
import { Html5Qrcode } from 'html5-qrcode';
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ScanBox } from './ScanBox';
import { useScanner } from './useScanner';
const qrcodeEleId = 'qrcode';
-export const QRCodeScannerInner = (props) => {
+export const QRCodeScannerInner = ({ setVisible }) => {
const containerRef = useRef
();
const imgUploaderRef = useRef();
const { t } = useTranslation('block-workbench');
@@ -23,9 +23,17 @@ export const QRCodeScannerInner = (props) => {
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
+ const onScanSuccess = useCallback(
+ (text) => {
+ setVisible(false);
+ },
+ [setVisible],
+ );
+
const { startScanFile } = useScanner({
onScannerSizeChanged: setOriginVideoSize,
elementId: qrcodeEleId,
+ onScanSuccess,
});
const getBoxStyle = (): React.CSSProperties => {
@@ -174,7 +182,7 @@ export const QRCodeScanner = (props) => {
return visible && cameraAvaliable ? (
-
+
setVisible(false)} />
{t('Scan QR code')}
diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/useScanner.ts b/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/useScanner.ts
index 7e4e5eae0b..16568498ce 100644
--- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/useScanner.ts
+++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/useScanner.ts
@@ -20,7 +20,7 @@ function removeStringIfStartsWith(text: string, prefix: string): string {
return text;
}
-export function useScanner({ onScannerSizeChanged, elementId }) {
+export function useScanner({ onScannerSizeChanged, elementId, onScanSuccess }) {
const app = useApp();
const mobileManager = app.pm.get(MobileManager);
const basename = mobileManager.mobileRouter.basename.replace(/\/+$/, '');
@@ -50,12 +50,17 @@ export function useScanner({ onScannerSizeChanged, elementId }) {
},
},
(text) => {
+ if (text?.startsWith('http')) {
+ window.location.href = text;
+ return;
+ }
navigate(removeStringIfStartsWith(text, basename));
+ onScanSuccess && onScanSuccess(text);
},
undefined,
);
},
- [navigate, onScannerSizeChanged, viewPoint, basename],
+ [navigate, onScannerSizeChanged, viewPoint, basename, onScanSuccess],
);
const stopScanner = useCallback(async (scanner: Html5Qrcode) => {
const state = scanner.getState();
@@ -69,13 +74,18 @@ export function useScanner({ onScannerSizeChanged, elementId }) {
await stopScanner(scanner);
try {
const { decodedText } = await scanner.scanFileV2(file, false);
+ if (decodedText?.startsWith('http')) {
+ window.location.href = decodedText;
+ return;
+ }
navigate(removeStringIfStartsWith(decodedText, basename));
+ onScanSuccess && onScanSuccess(decodedText);
} catch (error) {
alert(t('QR code recognition failed, please scan again'));
startScanCamera(scanner);
}
},
- [stopScanner, scanner, navigate, basename, t, startScanCamera],
+ [stopScanner, scanner, navigate, basename, t, startScanCamera, onScanSuccess],
);
useEffect(() => {
diff --git a/packages/plugins/@nocobase/plugin-calendar/package.json b/packages/plugins/@nocobase/plugin-calendar/package.json
index 94917103a9..3b4e77feb8 100644
--- a/packages/plugins/@nocobase/plugin-calendar/package.json
+++ b/packages/plugins/@nocobase/plugin-calendar/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-calendar",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "Calendar",
"displayName.zh-CN": "日历",
"description": "Provides callendar collection template and block for managing date data, typically for date/time related information such as events, appointments, tasks, and so on.",
diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx
index 0308569760..604b7de19b 100644
--- a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx
+++ b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx
@@ -108,7 +108,7 @@ const useEvents = (
title: string;
},
date: Date,
- view: (typeof Weeks)[number],
+ view: (typeof Weeks)[number] | any = 'month',
) => {
const parseExpression = useLazy(
() => import('cron-parser'),
@@ -132,8 +132,8 @@ const useEvents = (
const intervalTime = end.diff(start, 'millisecond', true);
const dateM = dayjs(date);
- const startDate = dateM.clone().startOf('month');
- const endDate = startDate.clone().endOf('month');
+ const startDate = dateM.clone().startOf(view);
+ const endDate = startDate.clone().endOf(view);
/**
* view === month 时,会显示当月日程
@@ -425,7 +425,6 @@ export const Calendar: any = withDynamicSchemaProps(
};
};
const BigCalendar = reactBigCalendar?.BigCalendar;
-
return wrapSSR(
diff --git a/packages/plugins/@nocobase/plugin-charts/package.json b/packages/plugins/@nocobase/plugin-charts/package.json
index 70c2b21626..6d91b5ba8c 100644
--- a/packages/plugins/@nocobase/plugin-charts/package.json
+++ b/packages/plugins/@nocobase/plugin-charts/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "图表(废弃)",
"description": "The plugin has been deprecated, please use the data visualization plugin instead.",
"description.zh-CN": "已废弃插件,请使用数据可视化插件代替。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"license": "AGPL-3.0",
"devDependencies": {
diff --git a/packages/plugins/@nocobase/plugin-client/package.json b/packages/plugins/@nocobase/plugin-client/package.json
index b0bef8efd9..5e9e25bc22 100644
--- a/packages/plugins/@nocobase/plugin-client/package.json
+++ b/packages/plugins/@nocobase/plugin-client/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "WEB 客户端",
"description": "Provides a client interface for the NocoBase server",
"description.zh-CN": "为 NocoBase 服务端提供客户端界面",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"license": "AGPL-3.0",
"devDependencies": {
diff --git a/packages/plugins/@nocobase/plugin-client/src/server/__tests__/desktopRoutes-union-role.test.ts b/packages/plugins/@nocobase/plugin-client/src/server/__tests__/desktopRoutes-union-role.test.ts
index 2d57e70cec..cddc03e9db 100644
--- a/packages/plugins/@nocobase/plugin-client/src/server/__tests__/desktopRoutes-union-role.test.ts
+++ b/packages/plugins/@nocobase/plugin-client/src/server/__tests__/desktopRoutes-union-role.test.ts
@@ -55,6 +55,12 @@ describe('Web client desktopRoutes', async () => {
},
});
+ await rootAgent.resource('roles').setSystemRoleMode({
+ values: {
+ roleMode: SystemRoleMode.allowUseUnion,
+ },
+ });
+
agent = await app.agent().login(user, UNION_ROLE_KEY);
});
diff --git a/packages/plugins/@nocobase/plugin-collection-sql/package.json b/packages/plugins/@nocobase/plugin-collection-sql/package.json
index 6b3e552fba..d32f59a53d 100644
--- a/packages/plugins/@nocobase/plugin-collection-sql/package.json
+++ b/packages/plugins/@nocobase/plugin-collection-sql/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表: SQL",
"description": "Provides SQL collection template",
"description.zh-CN": "提供 SQL 数据表模板",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"homepage": "https://docs-cn.nocobase.com/handbook/collection-sql",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/collection-sql",
"main": "dist/server/index.js",
diff --git a/packages/plugins/@nocobase/plugin-collection-tree/package.json b/packages/plugins/@nocobase/plugin-collection-tree/package.json
index ec049d4981..07ccedfe77 100644
--- a/packages/plugins/@nocobase/plugin-collection-tree/package.json
+++ b/packages/plugins/@nocobase/plugin-collection-tree/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-collection-tree",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "Collection: Tree",
"displayName.zh-CN": "数据表:树",
"description": "Provides tree collection template",
diff --git a/packages/plugins/@nocobase/plugin-data-source-main/package.json b/packages/plugins/@nocobase/plugin-data-source-main/package.json
index 1dec250e94..5ff106cf26 100644
--- a/packages/plugins/@nocobase/plugin-data-source-main/package.json
+++ b/packages/plugins/@nocobase/plugin-data-source-main/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "数据源:主数据库",
"description": "NocoBase main database, supports relational databases such as PostgreSQL, MySQL, MariaDB and so on.",
"description.zh-CN": "NocoBase 主数据库,支持 PostgreSQL、MySQL、MariaDB 等关系型数据库。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/data-source-main",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/data-source-main",
diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/package.json b/packages/plugins/@nocobase/plugin-data-source-manager/package.json
index 182ed026c4..af1b3ef74d 100644
--- a/packages/plugins/@nocobase/plugin-data-source-manager/package.json
+++ b/packages/plugins/@nocobase/plugin-data-source-manager/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-data-source-manager",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"displayName": "Data source manager",
"displayName.zh-CN": "数据源管理",
diff --git a/packages/plugins/@nocobase/plugin-data-visualization/package.json b/packages/plugins/@nocobase/plugin-data-visualization/package.json
index 6cd3bad639..0f928eeaba 100644
--- a/packages/plugins/@nocobase/plugin-data-visualization/package.json
+++ b/packages/plugins/@nocobase/plugin-data-visualization/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-data-visualization",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "Data visualization",
"displayName.zh-CN": "数据可视化",
"description": "Provides data visualization feature, including chart block and chart filter block, support line charts, area charts, bar charts and more than a dozen kinds of charts, you can also extend more chart types.",
diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/filter.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/filter.ts
index 38f63266bc..9bcfb4c46d 100644
--- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/filter.ts
+++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/filter.ts
@@ -135,11 +135,10 @@ export const useChartFilter = () => {
'x-decorator': 'ChartFilterFormItem',
'x-data-source': dataSource,
'x-collection-field': `${fieldName}.${field.name}`,
- ...defaultOperator?.schema,
'x-component-props': {
utc: false,
underFilter: true,
- ...field.uiSchema?.['x-component-props'],
+ component: defaultOperator?.schema?.['x-component'],
'filter-operator': defaultOperator,
'data-source': dataSource,
'collection-field': `${fieldName}.${field.name}`,
@@ -195,14 +194,15 @@ export const useChartFilter = () => {
'x-settings': 'chart:filterForm:item',
'x-toolbar': 'ChartFilterItemToolbar',
'x-decorator': 'ChartFilterFormItem',
+ 'x-component': 'CollectionField',
'x-data-source': dataSource,
'x-collection-field': `${fieldName}.${child.name}`,
...child.schema,
title,
- ...defaultOperator?.schema,
'x-component-props': {
utc: false,
underFilter: true,
+ component: defaultOperator?.schema?.['x-component'],
'filter-operator': defaultOperator,
'data-source': dataSource,
'collection-field': `${fieldName}.${child.name}`,
diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartFilterItem.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartFilterItem.tsx
index 81e7d04f28..d4855eface 100644
--- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartFilterItem.tsx
+++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartFilterItem.tsx
@@ -56,6 +56,7 @@ function useFieldComponentName(): string {
const map = {
// AssociationField 的 mode 默认值是 Select
AssociationField: 'Select',
+ 'DatePicker.FilterWithPicker': 'DatePicker',
};
const fieldComponentName =
fieldSchema?.['x-component-props']?.['mode'] ||
@@ -163,15 +164,15 @@ const EditOperator = () => {
const setOperatorComponent = (operator: any, component: any, props = {}) => {
const componentProps = field.componentProps || {};
- field.component = component;
field.componentProps = {
...componentProps,
+ component,
'filter-operator': operator,
...props,
};
- fieldSchema['x-component'] = component;
fieldSchema['x-component-props'] = {
...fieldSchema['x-component-props'],
+ component,
'filter-operator': operator,
...props,
};
@@ -179,9 +180,9 @@ const EditOperator = () => {
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
- 'x-component': component,
'x-component-props': {
...fieldSchema['x-component-props'],
+ component,
'filter-operator': operator,
...props,
},
diff --git a/packages/plugins/@nocobase/plugin-departments/.npmignore b/packages/plugins/@nocobase/plugin-departments/.npmignore
new file mode 100644
index 0000000000..65f5e8779f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/.npmignore
@@ -0,0 +1,2 @@
+/node_modules
+/src
diff --git a/packages/plugins/@nocobase/plugin-departments/README.md b/packages/plugins/@nocobase/plugin-departments/README.md
new file mode 100644
index 0000000000..cb4fb63505
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/README.md
@@ -0,0 +1 @@
+# @nocobase/plugin-department
diff --git a/packages/plugins/@nocobase/plugin-departments/client.d.ts b/packages/plugins/@nocobase/plugin-departments/client.d.ts
new file mode 100644
index 0000000000..6c459cbac4
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/client.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/client';
+export { default } from './dist/client';
diff --git a/packages/plugins/@nocobase/plugin-departments/client.js b/packages/plugins/@nocobase/plugin-departments/client.js
new file mode 100644
index 0000000000..b6e3be70e6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/client.js
@@ -0,0 +1 @@
+module.exports = require('./dist/client/index.js');
diff --git a/packages/plugins/@nocobase/plugin-departments/package.json b/packages/plugins/@nocobase/plugin-departments/package.json
new file mode 100644
index 0000000000..e6ba44c503
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/package.json
@@ -0,0 +1,20 @@
+{
+ "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.11",
+ "main": "dist/server/index.js",
+ "peerDependencies": {
+ "@nocobase/actions": "1.x",
+ "@nocobase/client": "1.x",
+ "@nocobase/plugin-user-data-sync": "1.x",
+ "@nocobase/server": "1.x",
+ "@nocobase/test": "1.x"
+ },
+ "keywords": [
+ "Users & permissions"
+ ],
+ "gitHead": "ce89d10eec858c413f60e001e83c7c8cf2645f5e"
+}
diff --git a/packages/plugins/@nocobase/plugin-departments/server.d.ts b/packages/plugins/@nocobase/plugin-departments/server.d.ts
new file mode 100644
index 0000000000..c41081ddc6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/server.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/server';
+export { default } from './dist/server';
diff --git a/packages/plugins/@nocobase/plugin-departments/server.js b/packages/plugins/@nocobase/plugin-departments/server.js
new file mode 100644
index 0000000000..972842039a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/server.js
@@ -0,0 +1 @@
+module.exports = require('./dist/server/index.js');
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/ResourcesProvider.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/ResourcesProvider.tsx
new file mode 100644
index 0000000000..2ad0daac83
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/ResourcesProvider.tsx
@@ -0,0 +1,110 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { CollectionProvider_deprecated, ResourceActionContext, TableBlockContext, useRequest } from '@nocobase/client';
+import React, { useContext, useEffect, useMemo } from 'react';
+import { departmentCollection } from './collections/departments';
+import { userCollection } from './collections/users';
+import { FormContext } from '@formily/react';
+import { createForm } from '@formily/core';
+
+export const ResourcesContext = React.createContext<{
+ user: any;
+ setUser?: (user: any) => void;
+ department: any; // department name
+ setDepartment?: (department: any) => void;
+ departmentsResource?: any;
+ usersResource?: any;
+}>({
+ user: {},
+ department: {},
+});
+
+export const ResourcesProvider: React.FC = (props) => {
+ const [user, setUser] = React.useState(null);
+ const [department, setDepartment] = React.useState(null);
+
+ const userService = useRequest({
+ resource: 'users',
+ action: 'list',
+ params: {
+ appends: ['departments', 'departments.parent(recursively=true)'],
+ filter: department
+ ? {
+ 'departments.id': department.id,
+ }
+ : {},
+ pageSize: 20,
+ },
+ });
+
+ useEffect(() => {
+ userService.run();
+ }, [department]);
+
+ const departmentRequest = {
+ resource: 'departments',
+ action: 'list',
+ params: {
+ paginate: false,
+ filter: {
+ parentId: null,
+ },
+ },
+ };
+ const departmentService = useRequest(departmentRequest);
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export const DepartmentsListProvider: React.FC = (props) => {
+ const { departmentsResource } = useContext(ResourcesContext);
+ const { service } = departmentsResource || {};
+ return (
+
+ {props.children}
+
+ );
+};
+
+export const UsersListProvider: React.FC = (props) => {
+ const { usersResource } = useContext(ResourcesContext);
+ const { service } = usersResource || {};
+ const form = useMemo(() => createForm(), []);
+ const field = form.createField({ name: 'table' });
+ return (
+
+
+ {props.children}
+
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/collections/departments.ts b/packages/plugins/@nocobase/plugin-departments/src/client/collections/departments.ts
new file mode 100644
index 0000000000..38105c0b2b
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/collections/departments.ts
@@ -0,0 +1,115 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+export const departmentCollection = {
+ name: 'departments',
+ fields: [
+ {
+ type: 'bigInt',
+ name: 'id',
+ primaryKey: true,
+ autoIncrement: true,
+ interface: 'id',
+ uiSchema: {
+ type: 'id',
+ title: '{{t("ID")}}',
+ },
+ },
+ {
+ name: 'title',
+ type: 'string',
+ interface: 'input',
+ uiSchema: {
+ type: 'string',
+ title: '{{t("Department name")}}',
+ 'x-component': 'Input',
+ required: true,
+ },
+ },
+ {
+ name: 'parent',
+ type: 'belongsTo',
+ interface: 'm2o',
+ collectionName: 'departments',
+ foreignKey: 'parentId',
+ target: 'departments',
+ targetKey: 'id',
+ treeParent: true,
+ uiSchema: {
+ title: '{{t("Superior department")}}',
+ 'x-component': 'DepartmentSelect',
+ // 'x-component-props': {
+ // multiple: false,
+ // fieldNames: {
+ // label: 'title',
+ // value: 'id',
+ // },
+ // },
+ },
+ },
+ {
+ interface: 'm2m',
+ type: 'belongsToMany',
+ name: 'roles',
+ target: 'roles',
+ collectionName: 'departments',
+ through: 'departmentsRoles',
+ foreignKey: 'departmentId',
+ otherKey: 'roleName',
+ targetKey: 'name',
+ sourceKey: 'id',
+ uiSchema: {
+ title: '{{t("Roles")}}',
+ 'x-component': 'AssociationField',
+ 'x-component-props': {
+ multiple: true,
+ fieldNames: {
+ label: 'title',
+ value: 'name',
+ },
+ },
+ },
+ },
+ {
+ interface: 'm2m',
+ type: 'belongsToMany',
+ name: 'owners',
+ collectionName: 'departments',
+ target: 'users',
+ through: 'departmentsUsers',
+ foreignKey: 'departmentId',
+ otherKey: 'userId',
+ targetKey: 'id',
+ sourceKey: 'id',
+ scope: {
+ isOwner: true,
+ },
+ uiSchema: {
+ title: '{{t("Owners")}}',
+ 'x-component': 'AssociationField',
+ 'x-component-props': {
+ multiple: true,
+ fieldNames: {
+ label: 'nickname',
+ value: 'id',
+ },
+ },
+ },
+ },
+ ],
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/collections/users.ts b/packages/plugins/@nocobase/plugin-departments/src/client/collections/users.ts
new file mode 100644
index 0000000000..ba309455a7
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/collections/users.ts
@@ -0,0 +1,142 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+export const userCollection = {
+ name: 'users',
+ fields: [
+ {
+ name: 'id',
+ type: 'bigInt',
+ autoIncrement: true,
+ primaryKey: true,
+ allowNull: false,
+ uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true },
+ interface: 'id',
+ },
+ {
+ interface: 'input',
+ type: 'string',
+ name: 'nickname',
+ uiSchema: {
+ type: 'string',
+ title: '{{t("Nickname")}}',
+ 'x-component': 'Input',
+ },
+ },
+ {
+ interface: 'input',
+ type: 'string',
+ name: 'username',
+ unique: true,
+ uiSchema: {
+ type: 'string',
+ title: '{{t("Username")}}',
+ 'x-component': 'Input',
+ 'x-validator': { username: true },
+ required: true,
+ },
+ },
+ {
+ interface: 'email',
+ type: 'string',
+ name: 'email',
+ unique: true,
+ uiSchema: {
+ type: 'string',
+ title: '{{t("Email")}}',
+ 'x-component': 'Input',
+ 'x-validator': 'email',
+ required: true,
+ },
+ },
+ {
+ interface: 'phone',
+ type: 'string',
+ name: 'phone',
+ unique: true,
+ uiSchema: {
+ type: 'string',
+ title: '{{t("Phone")}}',
+ 'x-component': 'Input',
+ 'x-validator': 'phone',
+ required: true,
+ },
+ },
+ {
+ interface: 'm2m',
+ type: 'belongsToMany',
+ name: 'roles',
+ target: 'roles',
+ foreignKey: 'userId',
+ otherKey: 'roleName',
+ onDelete: 'CASCADE',
+ sourceKey: 'id',
+ targetKey: 'name',
+ through: 'rolesUsers',
+ uiSchema: {
+ type: 'array',
+ title: '{{t("Roles")}}',
+ 'x-component': 'AssociationField',
+ 'x-component-props': {
+ multiple: true,
+ fieldNames: {
+ label: 'title',
+ value: 'name',
+ },
+ },
+ },
+ },
+ {
+ name: 'departments',
+ type: 'belongsToMany',
+ interface: 'm2m',
+ target: 'departments',
+ foreignKey: 'userId',
+ otherKey: 'departmentId',
+ onDelete: 'CASCADE',
+ sourceKey: 'id',
+ targetKey: 'id',
+ through: 'departmentsUsers',
+ uiSchema: {
+ type: 'array',
+ title: '{{t("Departments")}}',
+ 'x-component': 'DepartmentField',
+ },
+ },
+ {
+ interface: 'm2m',
+ type: 'belongsToMany',
+ name: 'mainDepartment',
+ target: 'departments',
+ foreignKey: 'userId',
+ otherKey: 'departmentId',
+ onDelete: 'CASCADE',
+ sourceKey: 'id',
+ targetKey: 'id',
+ through: 'departmentsUsers',
+ throughScope: {
+ isMain: true,
+ },
+ uiSchema: {
+ type: 'array',
+ title: '{{t("Main department")}}',
+ 'x-component': 'DepartmentField',
+ },
+ },
+ ],
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/DepartmentOwnersField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/components/DepartmentOwnersField.tsx
new file mode 100644
index 0000000000..afcb651250
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/DepartmentOwnersField.tsx
@@ -0,0 +1,35 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { SchemaSettings } from '@nocobase/client';
+import { enableLink, fieldComponent, titleField } from './fieldSettings';
+
+export const DepartmentOwnersFieldSettings = new SchemaSettings({
+ name: 'fieldSettings:component:DepartmentOwnersField',
+ items: [
+ {
+ ...fieldComponent,
+ },
+ {
+ ...titleField,
+ },
+ {
+ ...enableLink,
+ },
+ ],
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/ReadOnlyAssociationField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/components/ReadOnlyAssociationField.tsx
new file mode 100644
index 0000000000..d843d62847
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/ReadOnlyAssociationField.tsx
@@ -0,0 +1,27 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import React from 'react';
+import { useDepartmentTranslation } from '../locale';
+import { AssociationField } from '@nocobase/client';
+import { connect, mapReadPretty } from '@formily/react';
+
+export const ReadOnlyAssociationField = connect(() => {
+ const { t } = useDepartmentTranslation();
+ return {t('This field is currently not supported for use in form blocks.')}
;
+}, mapReadPretty(AssociationField.ReadPretty));
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/UserDepartmentsField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/components/UserDepartmentsField.tsx
new file mode 100644
index 0000000000..e866411db2
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/UserDepartmentsField.tsx
@@ -0,0 +1,35 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { SchemaSettings } from '@nocobase/client';
+import { enableLink, fieldComponent, titleField } from './fieldSettings';
+
+export const UserDepartmentsFieldSettings = new SchemaSettings({
+ name: 'fieldSettings:component:UserDepartmentsField',
+ items: [
+ {
+ ...fieldComponent,
+ },
+ {
+ ...titleField,
+ },
+ {
+ ...enableLink,
+ },
+ ],
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/UserMainDepartmentField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/components/UserMainDepartmentField.tsx
new file mode 100644
index 0000000000..0e8355e408
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/UserMainDepartmentField.tsx
@@ -0,0 +1,35 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { SchemaSettings } from '@nocobase/client';
+import { enableLink, fieldComponent, titleField } from './fieldSettings';
+
+export const UserMainDepartmentFieldSettings = new SchemaSettings({
+ name: 'fieldSettings:component:UserMainDepartmentField',
+ items: [
+ {
+ ...fieldComponent,
+ },
+ {
+ ...titleField,
+ },
+ {
+ ...enableLink,
+ },
+ ],
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/fieldSettings.ts b/packages/plugins/@nocobase/plugin-departments/src/client/components/fieldSettings.ts
new file mode 100644
index 0000000000..bc637afa40
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/fieldSettings.ts
@@ -0,0 +1,185 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import {
+ useCollectionField,
+ useCollectionManager_deprecated,
+ useCollection_deprecated,
+ useCompile,
+ useDesignable,
+ useFieldComponentName,
+ useFieldModeOptions,
+ useIsAddNewForm,
+ useTitleFieldOptions,
+} from '@nocobase/client';
+import { useDepartmentTranslation } from '../locale';
+import { Field } from '@formily/core';
+import { useField, useFieldSchema, ISchema } from '@formily/react';
+
+export const titleField: any = {
+ name: 'titleField',
+ type: 'select',
+ useComponentProps() {
+ const { t } = useDepartmentTranslation();
+ const field = useField();
+ const { dn } = useDesignable();
+ const options = useTitleFieldOptions();
+ const { uiSchema, fieldSchema: tableColumnSchema, collectionField: tableColumnField } = useColumnSchema();
+ const schema = useFieldSchema();
+ const fieldSchema = tableColumnSchema || schema;
+ const targetCollectionField = useCollectionField();
+ const collectionField = tableColumnField || targetCollectionField;
+ const fieldNames = {
+ ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
+ ...field?.componentProps?.fieldNames,
+ ...fieldSchema?.['x-component-props']?.['fieldNames'],
+ };
+ return {
+ title: t('Title field'),
+ options,
+ value: fieldNames?.label,
+ onChange(label) {
+ const schema = {
+ ['x-uid']: fieldSchema['x-uid'],
+ };
+ const newFieldNames = {
+ ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
+ ...fieldSchema['x-component-props']?.['fieldNames'],
+ label,
+ };
+ fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
+ fieldSchema['x-component-props']['fieldNames'] = newFieldNames;
+ schema['x-component-props'] = fieldSchema['x-component-props'];
+ field.componentProps.fieldNames = fieldSchema['x-component-props']?.fieldNames;
+ const path = field.path?.splice(field.path?.length - 1, 1);
+ field.form.query(`${path.concat(`*.` + fieldSchema.name)}`).forEach((f) => {
+ f.componentProps.fieldNames = fieldNames;
+ });
+ dn.emit('patch', {
+ schema,
+ });
+ dn.refresh();
+ },
+ };
+ },
+};
+
+export const isCollectionFieldComponent = (schema: ISchema) => {
+ return schema['x-component'] === 'CollectionField';
+};
+
+const useColumnSchema = () => {
+ const { getField } = useCollection_deprecated();
+ const compile = useCompile();
+ const columnSchema = useFieldSchema();
+ const { getCollectionJoinField } = useCollectionManager_deprecated();
+ const fieldSchema = columnSchema.reduceProperties((buf, s) => {
+ if (isCollectionFieldComponent(s)) {
+ return s;
+ }
+ return buf;
+ }, null);
+ if (!fieldSchema) {
+ return {};
+ }
+
+ const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']);
+ return { columnSchema, fieldSchema, collectionField, uiSchema: compile(collectionField?.uiSchema) };
+};
+
+export const enableLink = {
+ name: 'enableLink',
+ type: 'switch',
+ useVisible() {
+ const field = useField();
+ return field.readPretty;
+ },
+ useComponentProps() {
+ const { t } = useDepartmentTranslation();
+ const field = useField();
+ const { fieldSchema: tableColumnSchema } = useColumnSchema();
+ const schema = useFieldSchema();
+ const fieldSchema = tableColumnSchema || schema;
+ const { dn } = useDesignable();
+ return {
+ title: t('Enable link'),
+ checked: fieldSchema['x-component-props']?.enableLink !== false,
+ onChange(flag) {
+ fieldSchema['x-component-props'] = {
+ ...fieldSchema?.['x-component-props'],
+ enableLink: flag,
+ };
+ field.componentProps['enableLink'] = flag;
+ dn.emit('patch', {
+ schema: {
+ 'x-uid': fieldSchema['x-uid'],
+ 'x-component-props': {
+ ...fieldSchema?.['x-component-props'],
+ },
+ },
+ });
+ dn.refresh();
+ },
+ };
+ },
+};
+
+export const fieldComponent: any = {
+ name: 'fieldComponent',
+ type: 'select',
+ useComponentProps() {
+ const { t } = useDepartmentTranslation();
+ const field = useField();
+ const { fieldSchema: tableColumnSchema, collectionField } = useColumnSchema();
+ const schema = useFieldSchema();
+ const fieldSchema = tableColumnSchema || schema;
+ const fieldModeOptions = useFieldModeOptions({ fieldSchema: tableColumnSchema, collectionField });
+ // const isAddNewForm = useIsAddNewForm();
+ // const fieldMode = useFieldComponentName();
+ const { dn } = useDesignable();
+ return {
+ title: t('Field component'),
+ options: fieldModeOptions,
+ value: 'Select',
+ onChange(mode) {
+ const schema = {
+ ['x-uid']: fieldSchema['x-uid'],
+ };
+ fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
+ fieldSchema['x-component-props']['mode'] = mode;
+ schema['x-component-props'] = fieldSchema['x-component-props'];
+ field.componentProps = field.componentProps || {};
+ field.componentProps.mode = mode;
+
+ // 子表单状态不允许设置默认值
+ // if (isSubMode(fieldSchema) && isAddNewForm) {
+ // // @ts-ignore
+ // schema.default = null;
+ // fieldSchema.default = null;
+ // field?.setInitialValue?.(null);
+ // field?.setValue?.(null);
+ // }
+
+ void dn.emit('patch', {
+ schema,
+ });
+ dn.refresh();
+ },
+ };
+ },
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/components/index.ts b/packages/plugins/@nocobase/plugin-departments/src/client/components/index.ts
new file mode 100644
index 0000000000..a9a4c4c641
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/components/index.ts
@@ -0,0 +1,22 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+export * from './ReadOnlyAssociationField';
+export * from './UserDepartmentsField';
+export * from './UserMainDepartmentField';
+export * from './DepartmentOwnersField';
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/AggregateSearch.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/AggregateSearch.tsx
new file mode 100644
index 0000000000..fccec99e2b
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/AggregateSearch.tsx
@@ -0,0 +1,216 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import React, { useContext } from 'react';
+import { Input, Button, Empty, MenuProps, Dropdown, theme } from 'antd';
+import { useDepartmentTranslation } from '../locale';
+import { createStyles, useAPIClient, useRequest } from '@nocobase/client';
+import { ResourcesContext } from '../ResourcesProvider';
+
+const useStyles = createStyles(({ css }) => {
+ return {
+ searchDropdown: css`
+ .ant-dropdown-menu {
+ max-height: 500px;
+ overflow-y: scroll;
+ }
+ `,
+ };
+});
+
+export const AggregateSearch: React.FC = () => {
+ const { t } = useDepartmentTranslation();
+ const { token } = theme.useToken();
+ const { setDepartment, setUser } = useContext(ResourcesContext);
+ const [open, setOpen] = React.useState(false);
+ const [keyword, setKeyword] = React.useState('');
+ const [users, setUsers] = React.useState([]);
+ const [departments, setDepartments] = React.useState([]);
+ const [moreUsers, setMoreUsers] = React.useState(true);
+ const [moreDepartments, setMoreDepartments] = React.useState(true);
+ const { styles } = useStyles();
+ const limit = 10;
+
+ const api = useAPIClient();
+ const service = useRequest(
+ (params) =>
+ api
+ .resource('departments')
+ .aggregateSearch(params)
+ .then((res) => res?.data?.data),
+ {
+ manual: true,
+ onSuccess: (data, params) => {
+ const {
+ values: { type },
+ } = params[0] || {};
+ if (!data) {
+ return;
+ }
+ if ((!type || type === 'user') && data['users'].length < limit) {
+ setMoreUsers(false);
+ }
+ if ((!type || type === 'department') && data['departments'].length < limit) {
+ setMoreDepartments(false);
+ }
+ setUsers((users) => [...users, ...data['users']]);
+ setDepartments((departments) => [...departments, ...data['departments']]);
+ },
+ },
+ );
+ const { run } = service;
+
+ const handleSearch = (keyword: string) => {
+ setKeyword(keyword);
+ setUsers([]);
+ setDepartments([]);
+ setMoreUsers(true);
+ setMoreDepartments(true);
+ if (!keyword) {
+ return;
+ }
+ run({
+ values: { keyword, limit },
+ });
+ setOpen(true);
+ };
+
+ const handleChange = (e) => {
+ if (e.target.value) {
+ return;
+ }
+ setUser(null);
+ setKeyword('');
+ setOpen(false);
+ service.mutate({});
+ setUsers([]);
+ setDepartments([]);
+ };
+
+ const getTitle = (department: any) => {
+ const title = department.title;
+ const parent = department.parent;
+ if (parent) {
+ return getTitle(parent) + ' / ' + title;
+ }
+ return title;
+ };
+
+ const LoadMore: React.FC<{ type: string; last: number }> = (props) => {
+ return (
+ {
+ setOpen(true);
+ run({
+ values: { keyword, limit, ...props },
+ });
+ }}
+ >
+ {t('Load more')}
+
+ );
+ };
+
+ const getItems = () => {
+ const items: MenuProps['items'] = [];
+ if (!users.length && !departments.length) {
+ return [
+ {
+ key: '0',
+ label: ,
+ disabled: true,
+ },
+ ];
+ }
+ if (users.length) {
+ items.push({
+ key: '0',
+ type: 'group',
+ label: t('Users'),
+ children: users.map((user: { nickname: string; username: string; phone?: string; email?: string }) => ({
+ key: user.username,
+ label: (
+ setUser(user)}>
+
{user.nickname || user.username}
+
+ {`${user.username}${user.phone ? ' | ' + user.phone : ''}${user.email ? ' | ' + user.email : ''}`}
+
+
+ ),
+ })),
+ });
+ if (moreUsers) {
+ items.push({
+ type: 'group',
+ key: '0-loadMore',
+ label: ,
+ });
+ }
+ }
+ if (departments.length) {
+ items.push({
+ key: '1',
+ type: 'group',
+ label: t('Departments'),
+ children: departments.map((department: any) => ({
+ key: department.id,
+ label: setDepartment(department)}>{getTitle(department)}
,
+ })),
+ });
+ if (moreDepartments) {
+ items.push({
+ type: 'group',
+ key: '1-loadMore',
+ label: ,
+ });
+ }
+ }
+ return items;
+ };
+
+ return (
+ setOpen(open)}
+ >
+ {
+ if (!keyword) {
+ setOpen(false);
+ }
+ }}
+ onFocus={() => setDepartment(null)}
+ onSearch={handleSearch}
+ onChange={handleChange}
+ placeholder={t('Search for departments, users')}
+ style={{ marginBottom: '20px' }}
+ />
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/Department.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/Department.tsx
new file mode 100644
index 0000000000..b8673777ab
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/Department.tsx
@@ -0,0 +1,149 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import React, { createContext, useContext, useState } from 'react';
+import { Row, Button, Divider, theme } from 'antd';
+import { UserOutlined } from '@ant-design/icons';
+import { useDepartmentTranslation } from '../locale';
+import { NewDepartment } from './NewDepartment';
+import { DepartmentTree } from './DepartmentTree';
+import { ResourcesContext } from '../ResourcesProvider';
+import { AggregateSearch } from './AggregateSearch';
+import { useDepartmentManager } from '../hooks';
+import {
+ ActionContextProvider,
+ RecordProvider,
+ SchemaComponent,
+ SchemaComponentOptions,
+ useAPIClient,
+ useActionContext,
+ useRecord,
+ useResourceActionContext,
+} from '@nocobase/client';
+import { useForm, useField } from '@formily/react';
+import { DepartmentOwnersField } from './DepartmentOwnersField';
+
+export const DepartmentTreeContext = createContext({} as ReturnType);
+
+export const useCreateDepartment = () => {
+ const form = useForm();
+ const field = useField();
+ const ctx = useActionContext();
+ const { refreshAsync } = useResourceActionContext();
+ const api = useAPIClient();
+ const { expandedKeys, setLoadedKeys, setExpandedKeys } = useContext(DepartmentTreeContext);
+ return {
+ async run() {
+ try {
+ await form.submit();
+ field.data = field.data || {};
+ field.data.loading = true;
+ await api.resource('departments').create({ values: form.values });
+ ctx.setVisible(false);
+ await form.reset();
+ field.data.loading = false;
+ const expanded = [...expandedKeys];
+ setLoadedKeys([]);
+ setExpandedKeys([]);
+ await refreshAsync();
+ setExpandedKeys(expanded);
+ } catch (error) {
+ if (field.data) {
+ field.data.loading = false;
+ }
+ }
+ },
+ };
+};
+
+export const useUpdateDepartment = () => {
+ const field = useField();
+ const form = useForm();
+ const ctx = useActionContext();
+ const { refreshAsync } = useResourceActionContext();
+ const api = useAPIClient();
+ const { id: filterByTk } = useRecord() as any;
+ const { expandedKeys, setLoadedKeys, setExpandedKeys } = useContext(DepartmentTreeContext);
+ const { department, setDepartment } = useContext(ResourcesContext);
+ return {
+ async run() {
+ await form.submit();
+ field.data = field.data || {};
+ field.data.loading = true;
+ try {
+ await api.resource('departments').update({ filterByTk, values: form.values });
+ setDepartment({ department, ...form.values });
+ ctx.setVisible(false);
+ await form.reset();
+ const expanded = [...expandedKeys];
+ setLoadedKeys([]);
+ setExpandedKeys([]);
+ await refreshAsync();
+ setExpandedKeys(expanded);
+ } catch (e) {
+ console.log(e);
+ } finally {
+ field.data.loading = false;
+ }
+ },
+ };
+};
+
+export const Department: React.FC = () => {
+ const { t } = useDepartmentTranslation();
+ const [visible, setVisible] = useState(false);
+ const [drawer, setDrawer] = useState({} as any);
+ const { department, setDepartment } = useContext(ResourcesContext);
+ const { token } = theme.useToken();
+ const departmentManager = useDepartmentManager({
+ label: ({ node }) => ,
+ });
+
+ return (
+
+
+
+
+ }
+ style={{
+ textAlign: 'left',
+ marginBottom: '5px',
+ background: department ? '' : token.colorBgTextHover,
+ }}
+ onClick={() => {
+ setDepartment(null);
+ }}
+ block
+ >
+ {t('All users')}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentBlock.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentBlock.tsx
new file mode 100644
index 0000000000..01b379d59a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentBlock.tsx
@@ -0,0 +1,40 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import React from 'react';
+import { SchemaComponent } from '@nocobase/client';
+import { DepartmentManagement } from './DepartmentManagement';
+import { uid } from '@formily/shared';
+
+export const DepartmentBlock: React.FC = () => {
+ return (
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentField.tsx
new file mode 100644
index 0000000000..76763a15bb
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentField.tsx
@@ -0,0 +1,48 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import React, { useContext } from 'react';
+import { useField } from '@formily/react';
+import { Field } from '@formily/core';
+import { ResourcesContext } from '../ResourcesProvider';
+import { getDepartmentTitle } from '../utils';
+import { EllipsisWithTooltip } from '@nocobase/client';
+
+export const DepartmentField: React.FC = () => {
+ const { setDepartment } = useContext(ResourcesContext);
+ const field = useField();
+ const values = field.value || [];
+ const deptsMap = values.reduce((mp: { [id: number]: any }, dept: any) => {
+ mp[dept.id] = dept;
+ return mp;
+ }, {});
+ const depts = values.map((dept: { id: number; title: string }, index: number) => (
+
+ {
+ e.preventDefault();
+ setDepartment(deptsMap[dept.id]);
+ }}
+ >
+ {getDepartmentTitle(dept)}
+
+ {index !== values.length - 1 ? , : ''}
+
+ ));
+ return {depts} ;
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentManagement.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentManagement.tsx
new file mode 100644
index 0000000000..5bae972bf8
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentManagement.tsx
@@ -0,0 +1,44 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import React from 'react';
+import { Col, Row } from 'antd';
+import { Department } from './Department';
+import { Member } from './Member';
+import { SchemaComponentOptions } from '@nocobase/client';
+import { SuperiorDepartmentSelect, DepartmentSelect } from './DepartmentTreeSelect';
+import { DepartmentsListProvider, UsersListProvider } from '../ResourcesProvider';
+
+export const DepartmentManagement: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentOwnersField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentOwnersField.tsx
new file mode 100644
index 0000000000..6faffb38f3
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentOwnersField.tsx
@@ -0,0 +1,115 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import {
+ ActionContextProvider,
+ ResourceActionProvider,
+ SchemaComponent,
+ useActionContext,
+ useRecord,
+} from '@nocobase/client';
+import React, { useEffect, useRef, useState } from 'react';
+import { Select } from 'antd';
+import { Field } from '@formily/core';
+import { useField } from '@formily/react';
+import { departmentOwnersSchema } from './schemas/departments';
+
+export const DepartmentOwnersField: React.FC = () => {
+ const [visible, setVisible] = useState(false);
+ const department = useRecord() as any;
+ const field = useField();
+ const [value, setValue] = useState([]);
+ const selectedRows = useRef([]);
+ const handleSelect = (_: number[], rows: any[]) => {
+ selectedRows.current = rows;
+ };
+
+ const useSelectOwners = () => {
+ const { setVisible } = useActionContext();
+ return {
+ run() {
+ const selected = field.value || [];
+ field.setValue([...selected, ...selectedRows.current]);
+ selectedRows.current = [];
+ setVisible(false);
+ },
+ };
+ };
+
+ useEffect(() => {
+ if (!field.value) {
+ return;
+ }
+ setValue(
+ field.value.map((owner: any) => ({
+ value: owner.id,
+ label: owner.nickname || owner.username,
+ })),
+ );
+ }, [field.value]);
+
+ const RequestProvider: React.FC = (props) => (
+ owner.id),
+ },
+ }
+ : {},
+ },
+ }}
+ >
+ {props.children}
+
+ );
+
+ return (
+
+ {
+ 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)}
+ />
+
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTable.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTable.tsx
new file mode 100644
index 0000000000..e1e3a768d2
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTable.tsx
@@ -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
+ */
+
+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({});
+
+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();
+ 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();
+ 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 (
+ (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 (
+
+
+
+ {props.children}
+
+
+
+ );
+};
+
+export const DepartmentTable: React.FC<{
+ useDataSource: any;
+ useDisabled?: (record: any) => boolean;
+}> = ({ useDataSource, useDisabled }) => {
+ return (
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTree.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTree.tsx
new file mode 100644
index 0000000000..03ac4080b6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTree.tsx
@@ -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
+ */
+
+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;
+} = () => {
+ 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 (
+
+ {treeData?.length ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+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 (
+
+
{node.title}
+
+
+
+
+
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTreeSelect.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTreeSelect.tsx
new file mode 100644
index 0000000000..b66b511548
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTreeSelect.tsx
@@ -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
+ */
+
+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();
+ 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 (
+ {
+ 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 ;
+};
+
+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 ;
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/IsOwnerField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/IsOwnerField.tsx
new file mode 100644
index 0000000000..e685d2d89a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/IsOwnerField.tsx
@@ -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
+ */
+
+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 ;
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/Member.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/Member.tsx
new file mode 100644
index 0000000000..a23aa2bde8
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/Member.tsx
@@ -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
+ */
+
+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 (
+
+ {props.children}
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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 ? : null;
+};
+
+const MemberActions = () => {
+ const { department } = useContext(ResourcesContext);
+ return department ? : 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 ? {t(department?.title || 'All users')} : {t('Search results')} }
+
+ >
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/NewDepartment.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/NewDepartment.tsx
new file mode 100644
index 0000000000..08f4e78a9f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/NewDepartment.tsx
@@ -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
+ */
+
+import { SchemaComponent } from '@nocobase/client';
+import React from 'react';
+import { useDepartmentTranslation } from '../locale';
+
+export const NewDepartment: React.FC = () => {
+ const { t } = useDepartmentTranslation();
+ return (
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/UserDepartmentsField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/UserDepartmentsField.tsx
new file mode 100644
index 0000000000..e72869046a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/UserDepartmentsField.tsx
@@ -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
+ */
+
+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();
+ 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 (
+
+ <>
+ {(field?.value || []).map((dept) => (
+
+ {dept.title}
+ {dept.isMain ? (
+
+ {t('Main')}
+
+ ) : (
+ ''
+ )}
+ {/* {dept.isOwner ? ( */}
+ {/* */}
+ {/* {t('Owner')} */}
+ {/* */}
+ {/* ) : ( */}
+ {/* '' */}
+ {/* )} */}
+ handleClick(key, dept),
+ }}
+ >
+
+
+
+
+
+ ))}
+ } onClick={() => setVisible(true)} />
+ >
+
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/schemas/departments.ts b/packages/plugins/@nocobase/plugin-departments/src/client/departments/schemas/departments.ts
new file mode 100644
index 0000000000..32272647c7
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/schemas/departments.ts
@@ -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
+ */
+
+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 }}',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/schemas/users.ts b/packages/plugins/@nocobase/plugin-departments/src/client/departments/schemas/users.ts
new file mode 100644
index 0000000000..66c2115d0e
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/schemas/users.ts
@@ -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
+ */
+
+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 }}',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/hooks/departments-manager.ts b/packages/plugins/@nocobase/plugin-departments/src/client/hooks/departments-manager.ts
new file mode 100644
index 0000000000..57d99ee9aa
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/hooks/departments-manager.ts
@@ -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
+ */
+
+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,
+ };
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/hooks/index.ts b/packages/plugins/@nocobase/plugin-departments/src/client/hooks/index.ts
new file mode 100644
index 0000000000..72af4ce551
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/hooks/index.ts
@@ -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
+ */
+
+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';
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/hooks/tree-manager.ts b/packages/plugins/@nocobase/plugin-departments/src/client/hooks/tree-manager.ts
new file mode 100644
index 0000000000..d2ef32c348
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/hooks/tree-manager.ts
@@ -0,0 +1,142 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+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,
+ };
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/hooks/useTableBlockProps.ts b/packages/plugins/@nocobase/plugin-departments/src/client/hooks/useTableBlockProps.ts
new file mode 100644
index 0000000000..fe5b35eb8a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/hooks/useTableBlockProps.ts
@@ -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();
+ 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';
+}
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/index.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/index.tsx
new file mode 100644
index 0000000000..798fddecd6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/index.tsx
@@ -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
+ */
+
+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 (
+
+
+
+
+
+ );
+ },
+ 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;
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/locale.ts b/packages/plugins/@nocobase/plugin-departments/src/client/locale.ts
new file mode 100644
index 0000000000..2899c172d4
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/locale.ts
@@ -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
+ */
+
+import { useTranslation } from 'react-i18next';
+
+export function useDepartmentTranslation() {
+ return useTranslation(['departments', 'client'], { nsMode: 'fallback' });
+}
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/roles/RoleDepartmentsManager.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/roles/RoleDepartmentsManager.tsx
new file mode 100644
index 0000000000..30d8badc1d
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/roles/RoleDepartmentsManager.tsx
@@ -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
+ */
+
+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 (
+
+
+
+
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/roles/schemas/departments.ts b/packages/plugins/@nocobase/plugin-departments/src/client/roles/schemas/departments.ts
new file mode 100644
index 0000000000..240a37635c
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/roles/schemas/departments.ts
@@ -0,0 +1,171 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { 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 }}',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/utils.ts b/packages/plugins/@nocobase/plugin-departments/src/client/utils.ts
new file mode 100644
index 0000000000..52f6fb7663
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/client/utils.ts
@@ -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
+ */
+
+export const getDepartmentTitle = (record: any) => {
+ const title = record.title;
+ const parent = record.parent;
+ if (parent) {
+ return getDepartmentTitle(parent) + ' / ' + title;
+ }
+ return title;
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/index.ts b/packages/plugins/@nocobase/plugin-departments/src/index.ts
new file mode 100644
index 0000000000..7d69462f4f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/index.ts
@@ -0,0 +1,20 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+export * from './server';
+export { default } from './server';
diff --git a/packages/plugins/@nocobase/plugin-departments/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-departments/src/locale/en-US.json
new file mode 100644
index 0000000000..f872121a10
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/locale/en-US.json
@@ -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."
+}
diff --git a/packages/plugins/@nocobase/plugin-departments/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-departments/src/locale/zh-CN.json
new file mode 100644
index 0000000000..be5393b399
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/locale/zh-CN.json
@@ -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.": "该字段目前不支持在表单区块中使用。"
+}
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/actions.test.ts
new file mode 100644
index 0000000000..1cc536139b
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/actions.test.ts
@@ -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
+ */
+
+import { Database, Repository } from '@nocobase/database';
+import { MockServer, createMockServer } from '@nocobase/test';
+
+describe('actions', () => {
+ let app: MockServer;
+ let db: Database;
+ let repo: Repository;
+ let agent: any;
+
+ beforeAll(async () => {
+ app = await createMockServer({
+ plugins: ['field-sort', 'users', 'departments'],
+ });
+ db = app.db;
+ repo = db.getRepository('departments');
+ agent = app.agent();
+ });
+
+ afterAll(async () => {
+ await app.destroy();
+ });
+
+ afterEach(async () => {
+ await repo.destroy({ truncate: true });
+ });
+
+ it('should list users exclude department', async () => {
+ const dept = await repo.create({
+ values: {
+ title: 'Test department',
+ members: [1],
+ },
+ });
+ const res = await agent.resource('users').listExcludeDept({
+ departmentId: dept.id,
+ });
+ expect(res.status).toBe(200);
+ expect(res.body.data.length).toBe(0);
+ });
+
+ it('should list users exclude department with filter', async () => {
+ let res = await agent.resource('users').listExcludeDept({
+ departmentId: 1,
+ });
+ expect(res.status).toBe(200);
+ expect(res.body.data.length).toBe(1);
+
+ res = await agent.resource('users').listExcludeDept({
+ departmentId: 1,
+ filter: {
+ id: 1,
+ },
+ });
+ expect(res.status).toBe(200);
+ expect(res.body.data.length).toBe(1);
+
+ res = await agent.resource('users').listExcludeDept({
+ departmentId: 1,
+ filter: {
+ id: 2,
+ },
+ });
+ expect(res.status).toBe(200);
+ expect(res.body.data.length).toBe(0);
+ });
+
+ it('should set main department', async () => {
+ const depts = await repo.create({
+ values: [
+ {
+ title: 'Dept1',
+ members: [1],
+ },
+ {
+ title: 'Dept2',
+ members: [1],
+ },
+ ],
+ });
+ const deptUsers = db.getRepository('departmentsUsers');
+ await deptUsers.update({
+ filter: {
+ departmentId: depts[0].id,
+ userId: 1,
+ },
+ values: {
+ isMain: true,
+ },
+ });
+ const res = await agent.resource('users').setMainDepartment({
+ values: {
+ userId: 1,
+ departmentId: depts[1].id,
+ },
+ });
+ expect(res.status).toBe(200);
+ const records = await deptUsers.find({
+ filter: {
+ userId: 1,
+ },
+ });
+ const dept1 = records.find((record: any) => record.departmentId === depts[0].id);
+ const dept2 = records.find((record: any) => record.departmentId === depts[1].id);
+ expect(dept1.isMain).toBe(false);
+ expect(dept2.isMain).toBe(true);
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/data-sync.test.ts b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/data-sync.test.ts
new file mode 100644
index 0000000000..0471a66632
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/data-sync.test.ts
@@ -0,0 +1,278 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import PluginUserDataSyncServer, { UserDataResourceManager } from '@nocobase/plugin-user-data-sync';
+import { MockDatabase, MockServer, createMockServer } from '@nocobase/test';
+
+describe('department data sync', async () => {
+ let app: MockServer;
+ let db: MockDatabase;
+ let resourceManager: UserDataResourceManager;
+
+ beforeEach(async () => {
+ app = await createMockServer({
+ plugins: ['field-sort', 'user-data-sync', 'users', 'departments'],
+ });
+ db = app.db;
+ const plugin = app.pm.get('user-data-sync') as PluginUserDataSyncServer;
+ resourceManager = plugin.resourceManager;
+ });
+
+ afterEach(async () => {
+ await db.clean({ drop: true });
+ await app.destroy();
+ });
+
+ it('should create department', async () => {
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'department',
+ records: [
+ {
+ uid: '1',
+ title: 'test',
+ },
+ {
+ uid: '2',
+ title: 'sub-test',
+ parentUid: '1',
+ },
+ ],
+ });
+ const department = await db.getRepository('departments').findOne({
+ filter: {
+ title: 'test',
+ },
+ appends: ['children'],
+ });
+ expect(department).toBeTruthy();
+ expect(department.title).toBe('test');
+ expect(department.children).toHaveLength(1);
+ expect(department.children[0].title).toBe('sub-test');
+ });
+
+ it('should update department', async () => {
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'department',
+ records: [
+ {
+ uid: '1',
+ title: 'test',
+ },
+ ],
+ });
+ const department = await db.getRepository('departments').findOne({
+ filter: {
+ title: 'test',
+ },
+ appends: ['children'],
+ });
+ expect(department).toBeTruthy();
+ expect(department.children).toHaveLength(0);
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'department',
+ records: [
+ {
+ uid: '1',
+ title: 'test2',
+ },
+ {
+ uid: '2',
+ title: 'sub-test',
+ parentUid: '1',
+ },
+ ],
+ });
+ const department2 = await db.getRepository('departments').findOne({
+ filter: {
+ id: department.id,
+ },
+ appends: ['children'],
+ });
+ expect(department2).toBeTruthy();
+ expect(department2.title).toBe('test2');
+ expect(department2.children).toHaveLength(1);
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'department',
+ records: [
+ {
+ uid: '2',
+ title: 'sub-test',
+ },
+ ],
+ });
+ const department3 = await db.getRepository('departments').findOne({
+ filter: {
+ id: department2.children[0].id,
+ },
+ appends: ['parent'],
+ });
+ expect(department3).toBeTruthy();
+ expect(department3.parent).toBeNull();
+ });
+
+ it('should update user department', async () => {
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'department',
+ records: [
+ {
+ uid: '12',
+ title: 'test',
+ },
+ ],
+ });
+ const department = await db.getRepository('departments').findOne({
+ filter: {
+ title: 'test',
+ },
+ });
+ expect(department).toBeTruthy();
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'user',
+ records: [
+ {
+ uid: '1',
+ nickname: 'test',
+ email: 'test@nocobase.com',
+ departments: ['12'],
+ },
+ ],
+ });
+ const user = await db.getRepository('users').findOne({
+ filter: {
+ email: 'test@nocobase.com',
+ },
+ appends: ['departments'],
+ });
+ expect(user).toBeTruthy();
+ expect(user.departments).toHaveLength(1);
+ expect(user.departments[0].id).toBe(department.id);
+ const departmentUser = await db.getRepository('departmentsUsers').findOne({
+ filter: {
+ departmentId: department.id,
+ userId: user.id,
+ },
+ });
+ expect(departmentUser).toBeTruthy();
+ expect(departmentUser.isOwner).toBe(false);
+ expect(departmentUser.isMain).toBe(false);
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'user',
+ records: [
+ {
+ uid: '1',
+ nickname: 'test',
+ email: 'test@nocobase.com',
+ departments: [
+ {
+ uid: '12',
+ isOwner: true,
+ isMain: true,
+ },
+ ],
+ },
+ ],
+ });
+ const departmentUser2 = await db.getRepository('departmentsUsers').findOne({
+ filter: {
+ departmentId: department.id,
+ userId: user.id,
+ },
+ });
+ expect(departmentUser2).toBeTruthy();
+ expect(departmentUser2.isOwner).toBe(true);
+ expect(departmentUser2.isMain).toBe(true);
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'user',
+ records: [
+ {
+ uid: '2',
+ nickname: 'test2',
+ email: 'test2@nocobase.com',
+ departments: [
+ {
+ uid: '12',
+ isOwner: true,
+ isMain: false,
+ },
+ ],
+ },
+ ],
+ });
+ const user2 = await db.getRepository('users').findOne({
+ filter: {
+ email: 'test2@nocobase.com',
+ },
+ appends: ['departments'],
+ });
+ expect(user2).toBeTruthy();
+ expect(user2.departments).toHaveLength(1);
+ expect(user2.departments[0].id).toBe(department.id);
+ const departmentUser3 = await db.getRepository('departmentsUsers').findOne({
+ filter: {
+ departmentId: department.id,
+ userId: user2.id,
+ },
+ });
+ expect(departmentUser3).toBeTruthy();
+ expect(departmentUser3.isOwner).toBe(true);
+ expect(departmentUser3.isMain).toBe(false);
+ });
+
+ it('should update department custom field', async () => {
+ const departmemntCollection = db.getCollection('departments');
+ departmemntCollection.addField('customField', { type: 'string' });
+ await db.sync({
+ alter: true,
+ });
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'department',
+ records: [
+ {
+ uid: '1',
+ title: 'test',
+ customField: 'testField',
+ },
+ ],
+ });
+ const department = await db.getRepository('departments').findOne({
+ filter: {
+ title: 'test',
+ },
+ });
+ expect(department).toBeTruthy();
+ expect(department.customField).toBe('testField');
+ await resourceManager.updateOrCreate({
+ sourceName: 'test',
+ dataType: 'department',
+ records: [
+ {
+ uid: '1',
+ title: 'test',
+ customField: 'testField2',
+ },
+ ],
+ });
+ const department2 = await db.getRepository('departments').findOne({
+ filter: {
+ id: department.id,
+ },
+ });
+ expect(department2).toBeTruthy();
+ expect(department2.customField).toBe('testField2');
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/destroy-department-check.test.ts b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/destroy-department-check.test.ts
new file mode 100644
index 0000000000..80355b551e
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/destroy-department-check.test.ts
@@ -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
+ */
+
+import { Database, Repository } from '@nocobase/database';
+import { MockServer, createMockServer } from '@nocobase/test';
+
+describe('destroy department check', () => {
+ let app: MockServer;
+ let db: Database;
+ let repo: Repository;
+ let agent: any;
+
+ beforeAll(async () => {
+ app = await createMockServer({
+ plugins: ['field-sort', 'users', 'departments'],
+ });
+ db = app.db;
+ repo = db.getRepository('departments');
+ agent = app.agent();
+ });
+
+ afterAll(async () => {
+ await app.destroy();
+ });
+
+ afterEach(async () => {
+ await repo.destroy({ truncate: true });
+ });
+
+ it('should check if it has sub departments', async () => {
+ const dept = await repo.create({
+ values: {
+ title: 'Department',
+ children: [{ title: 'Sub department' }],
+ },
+ });
+ const res = await agent.resource('departments').destroy({
+ filterByTk: dept.id,
+ });
+ expect(res.status).toBe(400);
+ expect(res.text).toBe('The department has sub-departments, please delete them first');
+ });
+
+ it('should check if it has members', async () => {
+ const dept = await repo.create({
+ values: {
+ title: 'Department',
+ members: [1],
+ },
+ });
+ const res = await agent.resource('departments').destroy({
+ filterByTk: dept.id,
+ });
+ expect(res.status).toBe(400);
+ expect(res.text).toBe('The department has members, please remove them first');
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/set-department-owners.test.ts b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/set-department-owners.test.ts
new file mode 100644
index 0000000000..7775b6e873
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/set-department-owners.test.ts
@@ -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
+ */
+
+import { Database, Repository } from '@nocobase/database';
+import { MockServer, createMockServer } from '@nocobase/test';
+
+describe('set department owners', () => {
+ let app: MockServer;
+ let db: Database;
+ let repo: Repository;
+ let agent: any;
+
+ beforeAll(async () => {
+ app = await createMockServer({
+ plugins: ['field-sort', 'users', 'departments'],
+ });
+ db = app.db;
+ repo = db.getRepository('departments');
+ agent = app.agent();
+ });
+
+ afterAll(async () => {
+ await app.destroy();
+ });
+
+ afterEach(async () => {
+ await repo.destroy({ truncate: true });
+ });
+
+ it('should set department owners', async () => {
+ await db.getRepository('users').create({
+ values: {
+ username: 'test',
+ },
+ });
+ const dept = await repo.create({
+ values: {
+ title: 'Department',
+ members: [1, 2],
+ },
+ });
+ await agent.resource('departments').update({
+ filterByTk: dept.id,
+ values: {
+ owners: [{ id: 1 }],
+ },
+ });
+ const deptUser = await db.getRepository('departmentsUsers').findOne({
+ filter: {
+ userId: 1,
+ departmentId: dept.id,
+ },
+ });
+ expect(deptUser.isOwner).toBe(true);
+ await agent.resource('departments').update({
+ filterByTk: dept.id,
+ values: {
+ owners: [{ id: 2 }],
+ },
+ });
+ const deptUser1 = await db.getRepository('departmentsUsers').findOne({
+ filter: {
+ userId: 1,
+ departmentId: dept.id,
+ },
+ });
+ expect(deptUser1.isOwner).toBe(false);
+ const deptUser2 = await db.getRepository('departmentsUsers').findOne({
+ filter: {
+ userId: 2,
+ departmentId: dept.id,
+ },
+ });
+ expect(deptUser2.isOwner).toBe(true);
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/set-departments-info.test.ts b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/set-departments-info.test.ts
new file mode 100644
index 0000000000..b6417fbefa
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/set-departments-info.test.ts
@@ -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
+ */
+
+import { Database, Repository } from '@nocobase/database';
+import { MockServer, createMockServer } from '@nocobase/test';
+import { setDepartmentsInfo } from '../middlewares';
+
+describe('set departments info', () => {
+ let app: MockServer;
+ let db: Database;
+ let repo: Repository;
+ let agent: any;
+ let ctx: any;
+
+ beforeAll(async () => {
+ app = await createMockServer({
+ plugins: ['field-sort', 'users', 'departments', 'acl', 'data-source-manager'],
+ });
+ db = app.db;
+ repo = db.getRepository('departments');
+ agent = app.agent();
+ ctx = {
+ db,
+ cache: app.cache,
+ state: {},
+ };
+ });
+
+ afterAll(async () => {
+ await app.destroy();
+ });
+
+ afterEach(async () => {
+ await repo.destroy({ truncate: true });
+ });
+
+ it('should set departments roles', async () => {
+ ctx.state.currentUser = await db.getRepository('users').findOne({
+ filterByTk: 1,
+ });
+ const role = await db.getRepository('roles').create({
+ values: {
+ name: 'test-role',
+ title: 'Test role',
+ },
+ });
+ await repo.create({
+ values: {
+ title: 'Department',
+ roles: [role.name],
+ members: [1],
+ },
+ });
+ await setDepartmentsInfo(ctx, () => {});
+ expect(ctx.state.attachRoles.length).toBe(1);
+ expect(ctx.state.attachRoles[0].name).toBe('test-role');
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/set-main-department.test.ts b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/set-main-department.test.ts
new file mode 100644
index 0000000000..46a5b9d310
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/set-main-department.test.ts
@@ -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
+ */
+
+import { Database, Repository } from '@nocobase/database';
+import { MockServer, createMockServer } from '@nocobase/test';
+
+describe('set main department', () => {
+ let app: MockServer;
+ let db: Database;
+ let repo: Repository;
+ let agent: any;
+
+ beforeAll(async () => {
+ app = await createMockServer({
+ plugins: ['field-sort', 'users', 'departments'],
+ });
+ db = app.db;
+ repo = db.getRepository('departments');
+ agent = app.agent();
+ });
+
+ afterAll(async () => {
+ await app.destroy();
+ });
+
+ afterEach(async () => {
+ await repo.destroy({ truncate: true });
+ await db.getRepository('departmentsUsers').destroy({ truncate: true });
+ });
+
+ it('should set main department when add department members', async () => {
+ const dept = await repo.create({
+ values: {
+ title: 'Department',
+ },
+ });
+ await db.getRepository('users').create({
+ values: {
+ username: 'test',
+ },
+ });
+ await agent.resource('departments.members', dept.id).add({
+ values: [1, 2],
+ });
+ const throughRepo = db.getRepository('departmentsUsers');
+ const deptUsers = await throughRepo.find({
+ filter: {
+ userId: {
+ $in: [1, 2],
+ },
+ departmentId: dept.id,
+ },
+ });
+ for (const item of deptUsers) {
+ expect(item.isMain).toBe(true);
+ }
+
+ const dept2 = await repo.create({
+ values: {
+ title: 'Department2',
+ },
+ });
+ await agent.resource('departments.members', dept2.id).add({
+ values: [2],
+ });
+ const deptUsers2 = await throughRepo.find({
+ filter: {
+ userId: 2,
+ },
+ });
+ expect(deptUsers2.length).toBe(2);
+ expect(deptUsers2.find((i: any) => i.departmentId === dept.id).isMain).toBe(true);
+ expect(deptUsers2.find((i: any) => i.departmentId === dept2.id).isMain).toBe(false);
+ });
+
+ it('should set main department when remove department members', async () => {
+ const depts = await repo.create({
+ values: [
+ {
+ title: 'Department',
+ },
+ {
+ title: 'Department2',
+ },
+ ],
+ });
+ await agent.resource('departments.members', depts[0].id).add({
+ values: [1],
+ });
+ await agent.resource('departments.members', depts[1].id).add({
+ values: [1],
+ });
+ const throughRepo = db.getRepository('departmentsUsers');
+ const deptUsers = await throughRepo.find({
+ filter: {
+ userId: 1,
+ },
+ });
+ expect(deptUsers.length).toBe(2);
+ expect(deptUsers.find((i: any) => i.departmentId === depts[0].id).isMain).toBe(true);
+ expect(deptUsers.find((i: any) => i.departmentId === depts[1].id).isMain).toBe(false);
+
+ await agent.resource('departments.members', depts[0].id).remove({
+ values: [1],
+ });
+ const deptUsers2 = await throughRepo.find({
+ filter: {
+ userId: 1,
+ },
+ });
+ expect(deptUsers2.length).toBe(1);
+ expect(deptUsers2[0].departmentId).toBe(depts[1].id);
+ expect(deptUsers2[0].isMain).toBe(true);
+ });
+
+ it('should set main department when add user departments', async () => {
+ const depts = await repo.create({
+ values: [
+ {
+ title: 'Department',
+ },
+ {
+ title: 'Department2',
+ },
+ ],
+ });
+ await agent.resource('users.departments', 1).add({
+ values: depts.map((dept: any) => dept.id),
+ });
+ const throughRepo = db.getRepository('departmentsUsers');
+ const deptUsers = await throughRepo.find({
+ filter: {
+ userId: 1,
+ },
+ });
+ expect(deptUsers.length).toBe(2);
+ expect(deptUsers.find((i: any) => i.departmentId === depts[0].id).isMain).toBe(true);
+ expect(deptUsers.find((i: any) => i.departmentId === depts[1].id).isMain).toBe(false);
+ });
+
+ it('should set main department when remove user departments', async () => {
+ const depts = await repo.create({
+ values: [
+ {
+ title: 'Department',
+ },
+ {
+ title: 'Department2',
+ },
+ ],
+ });
+ await agent.resource('users.departments', 1).add({
+ values: depts.map((dept: any) => dept.id),
+ });
+ await agent.resource('users.departments', 1).remove({
+ values: [depts[0].id],
+ });
+ const throughRepo = db.getRepository('departmentsUsers');
+ const deptUsers = await throughRepo.find({
+ filter: {
+ userId: 1,
+ },
+ });
+ expect(deptUsers.length).toBe(1);
+ expect(deptUsers[0].departmentId).toBe(depts[1].id);
+ expect(deptUsers[0].isMain).toBe(true);
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/update-department-is-leaf.test.ts b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/update-department-is-leaf.test.ts
new file mode 100644
index 0000000000..7115d45456
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/__tests__/update-department-is-leaf.test.ts
@@ -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
+ */
+
+import { Database, Repository } from '@nocobase/database';
+import { MockServer, createMockServer } from '@nocobase/test';
+
+describe('update department isLeaf', () => {
+ let app: MockServer;
+ let db: Database;
+ let repo: Repository;
+ let agent: any;
+
+ beforeAll(async () => {
+ app = await createMockServer({
+ plugins: ['field-sort', 'users', 'departments'],
+ });
+ db = app.db;
+ repo = db.getRepository('departments');
+ agent = app.agent();
+ });
+
+ afterAll(async () => {
+ await app.destroy();
+ });
+
+ afterEach(async () => {
+ await repo.destroy({ truncate: true });
+ });
+
+ it('should update isLeaf when create sub department', async () => {
+ const res = await agent.resource('departments').create({
+ values: {
+ title: 'Department',
+ },
+ });
+ const dept = res.body.data;
+ expect(dept).toBeTruthy();
+ expect(dept.isLeaf).toBe(true);
+
+ await agent.resource('departments').create({
+ values: {
+ title: 'Sub Department',
+ parent: dept,
+ },
+ });
+ const record = await repo.findOne({
+ filterByTk: dept.id,
+ });
+ expect(record.isLeaf).toBe(false);
+ });
+
+ it.runIf(process.env.DB_DIALECT !== 'sqlite')('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);
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/actions/departments.ts b/packages/plugins/@nocobase/plugin-departments/src/server/actions/departments.ts
new file mode 100644
index 0000000000..280fd8ab18
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/actions/departments.ts
@@ -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
+ */
+
+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();
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/actions/users.ts b/packages/plugins/@nocobase/plugin-departments/src/server/actions/users.ts
new file mode 100644
index 0000000000..38681fa574
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/actions/users.ts
@@ -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
+ */
+
+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();
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/collections/.gitkeep b/packages/plugins/@nocobase/plugin-departments/src/server/collections/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/collections/departmentRoles.ts b/packages/plugins/@nocobase/plugin-departments/src/server/collections/departmentRoles.ts
new file mode 100644
index 0000000000..b0f44497c2
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/collections/departmentRoles.ts
@@ -0,0 +1,25 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { defineCollection } from '@nocobase/database';
+
+export default defineCollection({
+ name: 'departmentsRoles',
+ dumpRules: 'required',
+ migrationRules: ['overwrite'],
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/collections/departments.ts b/packages/plugins/@nocobase/plugin-departments/src/server/collections/departments.ts
new file mode 100644
index 0000000000..a5881b35c9
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/collections/departments.ts
@@ -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
+ */
+
+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,
+ ],
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/collections/departmentsUsers.ts b/packages/plugins/@nocobase/plugin-departments/src/server/collections/departmentsUsers.ts
new file mode 100644
index 0000000000..70d5534d2c
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/collections/departmentsUsers.ts
@@ -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
+ */
+
+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,
+ },
+ ],
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/collections/roles.ts b/packages/plugins/@nocobase/plugin-departments/src/server/collections/roles.ts
new file mode 100644
index 0000000000..bac06fa60b
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/collections/roles.ts
@@ -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
+ */
+
+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',
+ },
+ ],
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/collections/users.ts b/packages/plugins/@nocobase/plugin-departments/src/server/collections/users.ts
new file mode 100644
index 0000000000..c5bcf488cc
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/collections/users.ts
@@ -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
+ */
+
+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],
+});
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/department-data-sync-resource.ts b/packages/plugins/@nocobase/plugin-departments/src/server/department-data-sync-resource.ts
new file mode 100644
index 0000000000..69d0d414df
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/department-data-sync-resource.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ 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);
+ }
+ }
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/index.ts b/packages/plugins/@nocobase/plugin-departments/src/server/index.ts
new file mode 100644
index 0000000000..61787e8e89
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/index.ts
@@ -0,0 +1,19 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+export { default } from './plugin';
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/destroy-department-check.ts b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/destroy-department-check.ts
new file mode 100644
index 0000000000..ade251e3d9
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/destroy-department-check.ts
@@ -0,0 +1,48 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { 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();
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/index.ts b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/index.ts
new file mode 100644
index 0000000000..8d2e037c65
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/index.ts
@@ -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
+ */
+
+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';
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/reset-user-departments-cache.ts b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/reset-user-departments-cache.ts
new file mode 100644
index 0000000000..0606093be1
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/reset-user-departments-cache.ts
@@ -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
+ */
+
+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}`);
+ }
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/set-department-owners.ts b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/set-department-owners.ts
new file mode 100644
index 0000000000..56a0da774f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/set-department-owners.ts
@@ -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
+ */
+
+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();
+ }
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/set-departments-roles.ts b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/set-departments-roles.ts
new file mode 100644
index 0000000000..5171d69ae0
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/set-departments-roles.ts
@@ -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
+ */
+
+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();
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/set-main-department.ts b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/set-main-department.ts
new file mode 100644
index 0000000000..b5f7083fa3
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/set-main-department.ts
@@ -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
+ */
+
+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,
+ },
+ });
+ }
+ }
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/update-department-isleaf.ts b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/update-department-isleaf.ts
new file mode 100644
index 0000000000..9d0cbdf947
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/middlewares/update-department-isleaf.ts
@@ -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
+ */
+
+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();
+};
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/migrations/update-field-uischemas-20240307124823.ts b/packages/plugins/@nocobase/plugin-departments/src/server/migrations/update-field-uischemas-20240307124823.ts
new file mode 100644
index 0000000000..542d5865bf
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/migrations/update-field-uischemas-20240307124823.ts
@@ -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
+ */
+
+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,
+ },
+ });
+ }
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/models/department.ts b/packages/plugins/@nocobase/plugin-departments/src/server/models/department.ts
new file mode 100644
index 0000000000..fddd8fc0ce
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/models/department.ts
@@ -0,0 +1,31 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { Model } from '@nocobase/database';
+
+export class DepartmentModel extends Model {
+ getOwners() {
+ return this.getMembers({
+ through: {
+ where: {
+ isOwner: true,
+ },
+ },
+ });
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-departments/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-departments/src/server/plugin.ts
new file mode 100644
index 0000000000..6aeaf51fe3
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-departments/src/server/plugin.ts
@@ -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
+ */
+
+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('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;
diff --git a/packages/plugins/@nocobase/plugin-disable-pm-add/package.json b/packages/plugins/@nocobase/plugin-disable-pm-add/package.json
index 088b3f8d6c..d48830cb4b 100644
--- a/packages/plugins/@nocobase/plugin-disable-pm-add/package.json
+++ b/packages/plugins/@nocobase/plugin-disable-pm-add/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-disable-pm-add",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"peerDependencies": {
"@nocobase/client": "1.x",
diff --git a/packages/plugins/@nocobase/plugin-environment-variables/package.json b/packages/plugins/@nocobase/plugin-environment-variables/package.json
index 53f6b405b6..d46ff68b16 100644
--- a/packages/plugins/@nocobase/plugin-environment-variables/package.json
+++ b/packages/plugins/@nocobase/plugin-environment-variables/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-environment-variables",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"peerDependencies": {
"@nocobase/client": "1.x",
diff --git a/packages/plugins/@nocobase/plugin-error-handler/package.json b/packages/plugins/@nocobase/plugin-error-handler/package.json
index 99bb6de45b..fa41b1aa1f 100644
--- a/packages/plugins/@nocobase/plugin-error-handler/package.json
+++ b/packages/plugins/@nocobase/plugin-error-handler/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "错误处理器",
"description": "Handling application errors and exceptions.",
"description.zh-CN": "处理应用程序中的错误和异常。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"devDependencies": {
diff --git a/packages/plugins/@nocobase/plugin-error-handler/src/server/error-handler.ts b/packages/plugins/@nocobase/plugin-error-handler/src/server/error-handler.ts
index 45d27d7a05..0cf3286bbf 100644
--- a/packages/plugins/@nocobase/plugin-error-handler/src/server/error-handler.ts
+++ b/packages/plugins/@nocobase/plugin-error-handler/src/server/error-handler.ts
@@ -26,13 +26,16 @@ export class ErrorHandler {
message += `: ${err.cause.message}`;
}
+ const errorData: { message: string; code: string; title?: string } = {
+ message,
+ code: err.code,
+ };
+
+ if (err?.title) {
+ errorData.title = err.title;
+ }
ctx.body = {
- errors: [
- {
- message,
- code: err.code,
- },
- ],
+ errors: [errorData],
};
}
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/.npmignore b/packages/plugins/@nocobase/plugin-field-attachment-url/.npmignore
new file mode 100644
index 0000000000..65f5e8779f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/.npmignore
@@ -0,0 +1,2 @@
+/node_modules
+/src
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/README.md b/packages/plugins/@nocobase/plugin-field-attachment-url/README.md
new file mode 100644
index 0000000000..806a995fd7
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/README.md
@@ -0,0 +1 @@
+# @nocobase/plugin-field-attachment-url
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/client.d.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/client.d.ts
new file mode 100644
index 0000000000..6c459cbac4
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/client.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/client';
+export { default } from './dist/client';
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/client.js b/packages/plugins/@nocobase/plugin-field-attachment-url/client.js
new file mode 100644
index 0000000000..b6e3be70e6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/client.js
@@ -0,0 +1 @@
+module.exports = require('./dist/client/index.js');
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/package.json b/packages/plugins/@nocobase/plugin-field-attachment-url/package.json
new file mode 100644
index 0000000000..a174250d96
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@nocobase/plugin-field-attachment-url",
+ "version": "1.7.0-alpha.11",
+ "main": "dist/server/index.js",
+ "displayName": "Collection field: Attachment(URL)",
+ "displayName.zh-CN": "数据表字段:附件(URL)",
+ "description": "Supports attachments in URL format.",
+ "description.zh-CN": "支持 URL 格式的附件。",
+ "peerDependencies": {
+ "@nocobase/client": "1.x",
+ "@nocobase/plugin-file-manager": "1.x",
+ "@nocobase/server": "1.x",
+ "@nocobase/test": "1.x"
+ },
+ "keywords": [
+ "Collection fields"
+ ]
+}
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/server.d.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/server.d.ts
new file mode 100644
index 0000000000..c41081ddc6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/server.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/server';
+export { default } from './dist/server';
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/server.js b/packages/plugins/@nocobase/plugin-field-attachment-url/server.js
new file mode 100644
index 0000000000..972842039a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/server.js
@@ -0,0 +1 @@
+module.exports = require('./dist/server/index.js');
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/__e2e__/createField.test.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/__e2e__/createField.test.ts
new file mode 100644
index 0000000000..89e1432ff4
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/__e2e__/createField.test.ts
@@ -0,0 +1,28 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import { expect, test } from '@nocobase/test/e2e';
+
+test('create Attachment (URL) field', async ({ page }) => {
+ await page.goto('/admin/settings/data-source-manager/main/collections?type=main');
+ await page.getByLabel('action-Action.Link-Configure fields-collections-users', { exact: true }).click();
+ await page.getByRole('button', { name: 'plus Add field' }).click();
+ await page.getByRole('menuitem', { name: 'Attachment (URL)' }).click();
+ const displayName = `a${Math.random().toString(36).substring(7)}`;
+ const name = `a${Math.random().toString(36).substring(7)}`;
+ await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').fill(displayName);
+ await page.getByLabel('block-item-Input-fields-Field name').getByRole('textbox').fill(name);
+ await expect(page.getByLabel('block-item-RemoteSelect-')).toBeVisible();
+ await page.getByLabel('action-Action-Submit-fields-').click();
+ await expect(page.getByText(name)).toBeVisible();
+
+ // 删除
+ await page.getByLabel(`action-CollectionFields-Delete-fields-${name}`).click();
+ await page.getByRole('button', { name: 'OK', exact: true }).click();
+});
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/client.d.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/client.d.ts
new file mode 100644
index 0000000000..4e96f83fa1
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/client.d.ts
@@ -0,0 +1,249 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+// CSS modules
+type CSSModuleClasses = { readonly [key: string]: string };
+
+declare module '*.module.css' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.scss' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.sass' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.less' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.styl' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.stylus' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.pcss' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.sss' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+
+// CSS
+declare module '*.css' { }
+declare module '*.scss' { }
+declare module '*.sass' { }
+declare module '*.less' { }
+declare module '*.styl' { }
+declare module '*.stylus' { }
+declare module '*.pcss' { }
+declare module '*.sss' { }
+
+// Built-in asset types
+// see `src/node/constants.ts`
+
+// images
+declare module '*.apng' {
+ const src: string;
+ export default src;
+}
+declare module '*.png' {
+ const src: string;
+ export default src;
+}
+declare module '*.jpg' {
+ const src: string;
+ export default src;
+}
+declare module '*.jpeg' {
+ const src: string;
+ export default src;
+}
+declare module '*.jfif' {
+ const src: string;
+ export default src;
+}
+declare module '*.pjpeg' {
+ const src: string;
+ export default src;
+}
+declare module '*.pjp' {
+ const src: string;
+ export default src;
+}
+declare module '*.gif' {
+ const src: string;
+ export default src;
+}
+declare module '*.svg' {
+ const src: string;
+ export default src;
+}
+declare module '*.ico' {
+ const src: string;
+ export default src;
+}
+declare module '*.webp' {
+ const src: string;
+ export default src;
+}
+declare module '*.avif' {
+ const src: string;
+ export default src;
+}
+
+// media
+declare module '*.mp4' {
+ const src: string;
+ export default src;
+}
+declare module '*.webm' {
+ const src: string;
+ export default src;
+}
+declare module '*.ogg' {
+ const src: string;
+ export default src;
+}
+declare module '*.mp3' {
+ const src: string;
+ export default src;
+}
+declare module '*.wav' {
+ const src: string;
+ export default src;
+}
+declare module '*.flac' {
+ const src: string;
+ export default src;
+}
+declare module '*.aac' {
+ const src: string;
+ export default src;
+}
+declare module '*.opus' {
+ const src: string;
+ export default src;
+}
+declare module '*.mov' {
+ const src: string;
+ export default src;
+}
+declare module '*.m4a' {
+ const src: string;
+ export default src;
+}
+declare module '*.vtt' {
+ const src: string;
+ export default src;
+}
+
+// fonts
+declare module '*.woff' {
+ const src: string;
+ export default src;
+}
+declare module '*.woff2' {
+ const src: string;
+ export default src;
+}
+declare module '*.eot' {
+ const src: string;
+ export default src;
+}
+declare module '*.ttf' {
+ const src: string;
+ export default src;
+}
+declare module '*.otf' {
+ const src: string;
+ export default src;
+}
+
+// other
+declare module '*.webmanifest' {
+ const src: string;
+ export default src;
+}
+declare module '*.pdf' {
+ const src: string;
+ export default src;
+}
+declare module '*.txt' {
+ const src: string;
+ export default src;
+}
+
+// wasm?init
+declare module '*.wasm?init' {
+ const initWasm: (options?: WebAssembly.Imports) => Promise;
+ export default initWasm;
+}
+
+// web worker
+declare module '*?worker' {
+ const workerConstructor: {
+ new(options?: { name?: string }): Worker;
+ };
+ export default workerConstructor;
+}
+
+declare module '*?worker&inline' {
+ const workerConstructor: {
+ new(options?: { name?: string }): Worker;
+ };
+ export default workerConstructor;
+}
+
+declare module '*?worker&url' {
+ const src: string;
+ export default src;
+}
+
+declare module '*?sharedworker' {
+ const sharedWorkerConstructor: {
+ new(options?: { name?: string }): SharedWorker;
+ };
+ export default sharedWorkerConstructor;
+}
+
+declare module '*?sharedworker&inline' {
+ const sharedWorkerConstructor: {
+ new(options?: { name?: string }): SharedWorker;
+ };
+ export default sharedWorkerConstructor;
+}
+
+declare module '*?sharedworker&url' {
+ const src: string;
+ export default src;
+}
+
+declare module '*?raw' {
+ const src: string;
+ export default src;
+}
+
+declare module '*?url' {
+ const src: string;
+ export default src;
+}
+
+declare module '*?inline' {
+ const src: string;
+ export default src;
+}
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/component/AttachmentUrl.tsx b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/component/AttachmentUrl.tsx
new file mode 100644
index 0000000000..f8eb0f2c89
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/component/AttachmentUrl.tsx
@@ -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.
+ */
+
+import { RecursionField, connect, mapReadPretty, useField, useFieldSchema } from '@formily/react';
+import React, { useContext, useEffect, useState } from 'react';
+import {
+ FormProvider,
+ RecordPickerContext,
+ RecordPickerProvider,
+ SchemaComponentOptions,
+ useActionContext,
+ TableSelectorParamsProvider,
+ useTableSelectorProps as useTsp,
+ EllipsisWithTooltip,
+ CollectionProvider_deprecated,
+ useCollection_deprecated,
+ useCollectionManager_deprecated,
+ Upload,
+ useFieldNames,
+ ActionContextProvider,
+ AssociationField,
+ Input,
+} from '@nocobase/client';
+import schema from '../schema';
+import { useInsertSchema } from '../hook';
+
+const defaultToValueItem = (data) => {
+ return data?.thumbnailRule ? `${data?.url}${data?.thumbnailRule}` : data?.url;
+};
+
+const InnerAttachmentUrl = (props) => {
+ const { value, onChange, toValueItem = defaultToValueItem, disabled, underFilter, ...others } = props;
+ const fieldSchema = useFieldSchema();
+ const [visibleSelector, setVisibleSelector] = useState(false);
+ const [selectedRows, setSelectedRows] = useState([]);
+ const insertSelector = useInsertSchema('Selector');
+ const fieldNames = useFieldNames(props);
+ const field: any = useField();
+ const [options, setOptions] = useState();
+ const { getField } = useCollection_deprecated();
+ const collectionField = getField(field.props.name);
+ const { modalProps } = useActionContext();
+ const handleSelect = (ev) => {
+ ev.stopPropagation();
+ ev.preventDefault();
+ insertSelector(schema.Selector);
+ setVisibleSelector(true);
+ setSelectedRows([]);
+ };
+
+ useEffect(() => {
+ if (value && Object.keys(value).length > 0) {
+ setOptions(value);
+ } else {
+ setOptions(null);
+ }
+ }, [value, fieldNames?.label]);
+
+ const pickerProps = {
+ size: 'small',
+ fieldNames,
+ multiple: false,
+ association: {
+ target: collectionField?.target,
+ },
+ options,
+ onChange: props?.onChange,
+ selectedRows,
+ setSelectedRows,
+ collectionField,
+ };
+ const usePickActionProps = () => {
+ const { setVisible } = useActionContext();
+ const { selectedRows, onChange } = useContext(RecordPickerContext);
+ return {
+ onClick() {
+ onChange(toValueItem(selectedRows?.[0]) || null);
+ setVisible(false);
+ },
+ };
+ };
+ const useTableSelectorProps = () => {
+ const {
+ multiple,
+ options,
+ setSelectedRows,
+ selectedRows: rcSelectRows = [],
+ onChange,
+ } = useContext(RecordPickerContext);
+ const { onRowSelectionChange, rowKey = 'id', ...others } = useTsp();
+ const { setVisible } = useActionContext();
+ return {
+ ...others,
+ rowKey,
+ rowSelection: {
+ type: multiple ? 'checkbox' : 'radio',
+ selectedRowKeys: rcSelectRows?.filter((item) => options?.[rowKey] !== item[rowKey]).map((item) => item[rowKey]),
+ },
+ onRowSelectionChange(selectedRowKeys, selectedRows) {
+ setSelectedRows?.(selectedRows);
+ onRowSelectionChange?.(selectedRowKeys, selectedRows);
+ onChange(toValueItem(selectedRows?.[0]) || null);
+ setVisible(false);
+ },
+ };
+ };
+ if (underFilter) {
+ return ;
+ }
+ return (
+
+
+
+ {collectionField?.target && collectionField?.target !== 'attachments' && (
+
+
+
+
+
+ {
+ return s['x-component'] === 'AssociationField.Selector';
+ }}
+ />
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+const FileManageReadPretty = connect((props) => {
+ const { value } = props;
+ const fieldSchema = useFieldSchema();
+ const componentMode = fieldSchema?.['x-component-props']?.['componentMode'];
+ const { getField } = useCollection_deprecated();
+ const { getCollectionJoinField } = useCollectionManager_deprecated();
+ const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema['x-collection-field']);
+ if (componentMode === 'url') {
+ return {value} ;
+ }
+ return (
+ {collectionField ? : null}
+ );
+});
+
+export const AttachmentUrl = connect(InnerAttachmentUrl, mapReadPretty(FileManageReadPretty));
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/hook/index.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/hook/index.ts
new file mode 100644
index 0000000000..59389c8486
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/hook/index.ts
@@ -0,0 +1,84 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import { useFieldSchema, useField } from '@formily/react';
+import { useCollectionField, useDesignable, useRequest } from '@nocobase/client';
+import { cloneDeep, uniqBy } from 'lodash';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+
+function useStorageRules(storage) {
+ const name = storage ?? '';
+ const { loading, data } = useRequest(
+ {
+ url: `storages:getBasicInfo/${name}`,
+ },
+ {
+ refreshDeps: [name],
+ },
+ );
+ return (!loading && data?.data) || null;
+}
+export function useAttachmentUrlFieldProps(props) {
+ const field = useCollectionField();
+ const rules = useStorageRules(field?.storage);
+ return {
+ ...props,
+ rules,
+ action: `${field.target}:create${field.storage ? `?attachmentField=${field.collectionName}.${field.name}` : ''}`,
+ toValueItem: (data) => {
+ return data?.thumbnailRule ? `${data?.url}${data?.thumbnailRule}` : data?.url;
+ },
+ getThumbnailURL: (file) => {
+ return file?.url;
+ },
+ };
+}
+
+export const useInsertSchema = (component) => {
+ const fieldSchema = useFieldSchema();
+ const { insertAfterBegin } = useDesignable();
+ const insert = useCallback(
+ (ss) => {
+ const schema = fieldSchema.reduceProperties((buf, s) => {
+ if (s['x-component'] === 'AssociationField.' + component) {
+ return s;
+ }
+ return buf;
+ }, null);
+ if (!schema) {
+ insertAfterBegin(cloneDeep(ss));
+ }
+ },
+ [component, fieldSchema, insertAfterBegin],
+ );
+ return insert;
+};
+
+export const useAttachmentTargetProps = () => {
+ const { t } = useTranslation();
+ const field = useField();
+ return {
+ service: {
+ resource: 'collections:listFileCollectionsWithPublicStorage',
+ params: {
+ paginate: false,
+ },
+ },
+ manual: false,
+ fieldNames: {
+ label: 'title',
+ value: 'name',
+ },
+ onSuccess: (data) => {
+ field.data = field.data || {};
+ field.data.options = data?.data;
+ },
+ };
+};
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/index.tsx b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/index.tsx
new file mode 100644
index 0000000000..c535c97f67
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/index.tsx
@@ -0,0 +1,38 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import { Plugin, lazy } from '@nocobase/client';
+import { AttachmentURLFieldInterface } from './interfaces/attachment-url';
+import { useAttachmentUrlFieldProps } from './hook';
+// import { AttachmentUrl } from './component/AttachmentUrl';
+const { AttachmentUrl } = lazy(() => import('./component/AttachmentUrl'), 'AttachmentUrl');
+
+import { attachmentUrlComponentFieldSettings } from './settings';
+export class PluginFieldAttachmentUrlClient extends Plugin {
+ async afterAdd() {
+ // await this.app.pm.add()
+ }
+
+ async beforeLoad() {}
+
+ // You can get and modify the app instance here
+ async load() {
+ this.app.dataSourceManager.addFieldInterfaces([AttachmentURLFieldInterface]);
+ this.app.addScopes({ useAttachmentUrlFieldProps });
+
+ this.app.addComponents({ AttachmentUrl });
+ this.app.schemaSettingsManager.add(attachmentUrlComponentFieldSettings);
+
+ // this.app.addProvider()
+ // this.app.addProviders()
+ // this.app.router.add()
+ }
+}
+
+export default PluginFieldAttachmentUrlClient;
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/interfaces/attachment-url.tsx b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/interfaces/attachment-url.tsx
new file mode 100644
index 0000000000..464f2a8c57
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/interfaces/attachment-url.tsx
@@ -0,0 +1,85 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import { CollectionFieldInterface, interfacesProperties } from '@nocobase/client';
+import { ISchema } from '@formily/react';
+import { useAttachmentTargetProps } from '../hook';
+import { tStr } from '../locale';
+
+const { defaultProps, operators } = interfacesProperties;
+
+export const defaultToolbar = [
+ 'headings',
+ 'bold',
+ 'italic',
+ 'strike',
+ 'link',
+ 'list',
+ 'ordered-list',
+ 'check',
+ 'quote',
+ 'line',
+ 'code',
+ 'inline-code',
+ 'upload',
+ 'fullscreen',
+];
+
+export class AttachmentURLFieldInterface extends CollectionFieldInterface {
+ name = 'attachmentURL';
+ type = 'object';
+ group = 'media';
+ title = tStr('Attachment (URL)');
+ default = {
+ type: 'string',
+ // name,
+ uiSchema: {
+ type: 'string',
+ // title,
+ 'x-component': 'AttachmentUrl',
+ 'x-use-component-props': 'useAttachmentUrlFieldProps',
+ },
+ };
+ availableTypes = ['string', 'text'];
+ properties = {
+ ...defaultProps,
+ target: {
+ required: true,
+ type: 'string',
+ title: tStr('Which file collection should it be uploaded to'),
+ 'x-decorator': 'FormItem',
+ 'x-component': 'RemoteSelect',
+ 'x-use-component-props': useAttachmentTargetProps,
+ 'x-reactions': (field) => {
+ const options = field.data?.options || [];
+ const hasAttachments = options.some((opt) => opt?.name === 'attachments');
+ if (hasAttachments) {
+ !field.initialValue && field.setInitialValue('attachments');
+ }
+ },
+ },
+ targetKey: {
+ 'x-hidden': true,
+ default: 'id',
+ type: 'string',
+ },
+ };
+ schemaInitialize(schema: ISchema, { block }) {
+ schema['x-component-props'] = schema['x-component-props'] || {};
+ schema['x-component-props']['mode'] = 'AttachmentUrl';
+ if (['Table', 'Kanban'].includes(block)) {
+ schema['x-component-props']['ellipsis'] = true;
+ schema['x-component-props']['size'] = 'small';
+ }
+ }
+ filterable = {
+ operators: operators.bigField,
+ };
+ titleUsable = true;
+}
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/locale.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/locale.ts
new file mode 100644
index 0000000000..a26dd0158f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/locale.ts
@@ -0,0 +1,30 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+// @ts-ignore
+import pkg from '../../package.json';
+import { useApp } from '@nocobase/client';
+import { useTranslation } from 'react-i18next';
+
+export const NAMESPACE = 'attachmentUrl';
+
+export function useT() {
+ const app = useApp();
+ return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] });
+}
+
+export function tStr(key: string) {
+ return `{{t(${JSON.stringify(key)}, { ns: ['${pkg.name}', 'client'], nsMode: 'fallback' })}}`;
+}
+
+export function useAttachmentUrlTranslation() {
+ return useTranslation([NAMESPACE, 'client'], {
+ nsMode: 'fallback',
+ });
+}
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/schema.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/schema.ts
new file mode 100644
index 0000000000..f5ae80d0ce
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/schema.ts
@@ -0,0 +1,53 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+export default {
+ Selector: {
+ type: 'void',
+ 'x-component': 'AssociationField.Selector',
+ title: '{{ t("Select record") }}',
+ 'x-component-props': {
+ className: 'nb-record-picker-selector',
+ },
+ properties: {
+ grid: {
+ type: 'void',
+ 'x-component': 'Grid',
+ 'x-initializer': 'popup:tableSelector:addBlock',
+ properties: {},
+ },
+ footer: {
+ 'x-component': 'Action.Container.Footer',
+ 'x-component-props': {},
+ properties: {
+ actions: {
+ type: 'void',
+ 'x-component': 'ActionBar',
+ 'x-component-props': {},
+ properties: {
+ submit: {
+ title: '{{ t("Submit") }}',
+ 'x-action': 'submit',
+ 'x-component': 'Action',
+ 'x-use-component-props': 'usePickActionProps',
+ // 'x-designer': 'Action.Designer',
+ 'x-toolbar': 'ActionSchemaToolbar',
+ 'x-settings': 'actionSettings:submit',
+ 'x-component-props': {
+ type: 'primary',
+ htmlType: 'submit',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/settings/index.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/settings/index.ts
new file mode 100644
index 0000000000..52463073c2
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/client/settings/index.ts
@@ -0,0 +1,171 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import { Field } from '@formily/core';
+import { useField, useFieldSchema, useForm } from '@formily/react';
+import { useTranslation } from 'react-i18next';
+import { useColumnSchema, useIsFieldReadPretty, SchemaSettings, useDesignable } from '@nocobase/client';
+
+const fieldComponent: any = {
+ name: 'fieldComponent',
+ type: 'select',
+ useComponentProps() {
+ const { t } = useTranslation();
+ const field = useField();
+ const { fieldSchema: tableColumnSchema } = useColumnSchema();
+ const schema = useFieldSchema();
+ const fieldSchema = tableColumnSchema || schema;
+ const { dn } = useDesignable();
+
+ return {
+ title: t('Field component'),
+ options: [
+ { label: t('URL'), value: 'url' },
+ { label: t('Preview'), value: 'preview' },
+ ],
+ value: fieldSchema['x-component-props']['componentMode'] || 'preview',
+ onChange(componentMode) {
+ const schema = {
+ ['x-uid']: fieldSchema['x-uid'],
+ };
+ fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
+ fieldSchema['x-component-props']['componentMode'] = componentMode;
+ schema['x-component-props'] = fieldSchema['x-component-props'];
+ field.componentProps = field.componentProps || {};
+ field.componentProps.componentMode = componentMode;
+ void dn.emit('patch', {
+ schema,
+ });
+ dn.refresh();
+ },
+ };
+ },
+ useVisible() {
+ const readPretty = useIsFieldReadPretty();
+ const { fieldSchema: tableColumnSchema } = useColumnSchema();
+ return readPretty;
+ },
+};
+
+export const attachmentUrlComponentFieldSettings = new SchemaSettings({
+ name: 'fieldSettings:component:AttachmentUrl',
+ items: [
+ {
+ name: 'quickUpload',
+ type: 'switch',
+ useComponentProps() {
+ const { t } = useTranslation();
+ const field = useField();
+ const { fieldSchema: tableColumnSchema } = useColumnSchema();
+ const schema = useFieldSchema();
+ const fieldSchema = tableColumnSchema || schema;
+ const { dn, refresh } = useDesignable();
+ return {
+ title: t('Quick upload'),
+ checked: fieldSchema['x-component-props']?.quickUpload !== (false as boolean),
+ onChange(value) {
+ const schema = {
+ ['x-uid']: fieldSchema['x-uid'],
+ };
+ field.componentProps.quickUpload = value;
+ fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
+ fieldSchema['x-component-props'].quickUpload = value;
+ schema['x-component-props'] = fieldSchema['x-component-props'];
+ dn.emit('patch', {
+ schema,
+ });
+ refresh();
+ },
+ };
+ },
+ useVisible() {
+ const { fieldSchema: tableColumnSchema } = useColumnSchema();
+ const field = useField();
+ const form = useForm();
+ const isReadPretty = tableColumnSchema?.['x-read-pretty'] || field.readPretty || form.readPretty;
+ return !isReadPretty && !field.componentProps.underFilter;
+ },
+ },
+ {
+ name: 'selectFile',
+ type: 'switch',
+ useComponentProps() {
+ const { t } = useTranslation();
+ const field = useField();
+ const { fieldSchema: tableColumnSchema } = useColumnSchema();
+ const schema = useFieldSchema();
+ const fieldSchema = tableColumnSchema || schema;
+ const { dn, refresh } = useDesignable();
+ return {
+ title: t('Select file'),
+ checked: fieldSchema['x-component-props']?.selectFile !== (false as boolean),
+ onChange(value) {
+ const schema = {
+ ['x-uid']: fieldSchema['x-uid'],
+ };
+ field.componentProps.selectFile = value;
+ fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
+ fieldSchema['x-component-props'].selectFile = value;
+ schema['x-component-props'] = fieldSchema['x-component-props'];
+ dn.emit('patch', {
+ schema,
+ });
+ refresh();
+ },
+ };
+ },
+ useVisible() {
+ const { fieldSchema: tableColumnSchema } = useColumnSchema();
+ const field = useField();
+ const form = useForm();
+ const isReadPretty = tableColumnSchema?.['x-read-pretty'] || field.readPretty || form.readPretty;
+ return !isReadPretty && !field.componentProps.underFilter;
+ },
+ },
+ fieldComponent,
+ {
+ name: 'size',
+ type: 'select',
+ useVisible() {
+ const readPretty = useIsFieldReadPretty();
+ const { fieldSchema: tableColumnSchema } = useColumnSchema();
+ return readPretty && !tableColumnSchema;
+ },
+ useComponentProps() {
+ const { t } = useTranslation();
+ const field = useField();
+ const fieldSchema = useFieldSchema();
+ const { dn } = useDesignable();
+ return {
+ title: t('Size'),
+ options: [
+ { label: t('Large'), value: 'large' },
+ { label: t('Default'), value: 'default' },
+ { label: t('Small'), value: 'small' },
+ ],
+ value: field?.componentProps?.size || 'default',
+ onChange(size) {
+ const schema = {
+ ['x-uid']: fieldSchema['x-uid'],
+ };
+ fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
+ fieldSchema['x-component-props']['size'] = size;
+ schema['x-component-props'] = fieldSchema['x-component-props'];
+ field.componentProps = field.componentProps || {};
+ field.componentProps.size = size;
+ dn.emit('patch', {
+ schema,
+ });
+ dn.refresh();
+ },
+ };
+ },
+ },
+ ],
+});
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/index.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/index.ts
new file mode 100644
index 0000000000..be99a2ff1a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/index.ts
@@ -0,0 +1,11 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+export * from './server';
+export { default } from './server';
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/en-US.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/en-US.json
@@ -0,0 +1 @@
+{}
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/zh-CN.json
new file mode 100644
index 0000000000..78def6ea14
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/locale/zh-CN.json
@@ -0,0 +1,4 @@
+{
+ "Which file collection should it be uploaded to":"上传到文件表",
+ "Attachment (URL)":"附件 (URL)"
+}
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/collections/.gitkeep b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/collections/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/index.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/index.ts
new file mode 100644
index 0000000000..be989de7c3
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/index.ts
@@ -0,0 +1,10 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+export { default } from './plugin';
diff --git a/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/plugin.ts
new file mode 100644
index 0000000000..648ed0cec0
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-field-attachment-url/src/server/plugin.ts
@@ -0,0 +1,65 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import { Plugin } from '@nocobase/server';
+import PluginFileManagerServer from '@nocobase/plugin-file-manager';
+
+export class PluginFieldAttachmentUrlServer extends Plugin {
+ async afterAdd() {}
+
+ async beforeLoad() {}
+
+ async load() {
+ this.app.resourceManager.registerActionHandlers({
+ 'collections:listFileCollectionsWithPublicStorage': async (ctx, next) => {
+ const fileCollections = await this.db.getRepository('collections').find({
+ filter: {
+ 'options.template': 'file',
+ },
+ });
+
+ const filePlugin = this.pm.get('file-manager') as PluginFileManagerServer | any;
+
+ const options = [];
+
+ const fileCollection = this.db.getCollection('attachments');
+
+ if (await filePlugin.isPublicAccessStorage(fileCollection?.options?.storage)) {
+ options.push({
+ title: '{{t("Attachments")}}',
+ name: 'attachments',
+ });
+ }
+
+ for (const fileCollection of fileCollections) {
+ if (await filePlugin.isPublicAccessStorage(fileCollection?.options?.storage)) {
+ options.push({
+ name: fileCollection.name,
+ title: fileCollection.title,
+ });
+ }
+ }
+
+ ctx.body = options;
+
+ await next();
+ },
+ });
+ }
+
+ async install() {}
+
+ async afterEnable() {}
+
+ async afterDisable() {}
+
+ async remove() {}
+}
+
+export default PluginFieldAttachmentUrlServer;
diff --git a/packages/plugins/@nocobase/plugin-field-china-region/package.json b/packages/plugins/@nocobase/plugin-field-china-region/package.json
index 9341292ce0..cf65196fb5 100644
--- a/packages/plugins/@nocobase/plugin-field-china-region/package.json
+++ b/packages/plugins/@nocobase/plugin-field-china-region/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-field-china-region",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "Collection field: administrative divisions of China",
"displayName.zh-CN": "数据表字段:中国行政区划",
"description": "Provides data and field type for administrative divisions of China.",
diff --git a/packages/plugins/@nocobase/plugin-field-formula/package.json b/packages/plugins/@nocobase/plugin-field-formula/package.json
index 062285d9cc..b7e3b1e03c 100644
--- a/packages/plugins/@nocobase/plugin-field-formula/package.json
+++ b/packages/plugins/@nocobase/plugin-field-formula/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表字段:公式",
"description": "Configure and store the results of calculations between multiple field values in the same record, supporting both Math.js and Excel formula functions.",
"description.zh-CN": "可以配置并存储同一条记录的多字段值之间的计算结果,支持 Math.js 和 Excel formula functions 两种引擎",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/field-formula",
diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/package.json b/packages/plugins/@nocobase/plugin-field-m2m-array/package.json
index ff4c75ad9a..65621e764a 100644
--- a/packages/plugins/@nocobase/plugin-field-m2m-array/package.json
+++ b/packages/plugins/@nocobase/plugin-field-m2m-array/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表字段:多对多 (数组)",
"description": "Allows to create many to many relationships between two models by storing an array of unique keys of the target model.",
"description.zh-CN": "支持通过在数组中存储目标表唯一键的方式建立多对多关系。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"peerDependencies": {
"@nocobase/client": "1.x",
diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/issues.test.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/issues.test.ts
index 536d72d1a5..94b2f559b3 100644
--- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/issues.test.ts
+++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/issues.test.ts
@@ -324,4 +324,100 @@ describe('issues', () => {
}
expect(res.status).toBe(200);
});
+
+ test('filtering by fields of a relation collection with m2m array field', async () => {
+ await db.getRepository('collections').create({
+ values: {
+ name: 'tags',
+ fields: [
+ {
+ name: 'id',
+ type: 'bigInt',
+ autoIncrement: true,
+ primaryKey: true,
+ allowNull: false,
+ },
+ {
+ name: 'title',
+ type: 'string',
+ },
+ ],
+ },
+ });
+ await db.getRepository('collections').create({
+ values: {
+ name: 'users',
+ fields: [
+ {
+ name: 'id',
+ type: 'bigInt',
+ autoIncrement: true,
+ primaryKey: true,
+ allowNull: false,
+ },
+ {
+ name: 'username',
+ type: 'string',
+ },
+ {
+ name: 'tags',
+ type: 'belongsToArray',
+ foreignKey: 'tag_ids',
+ target: 'tags',
+ targetKey: 'id',
+ },
+ ],
+ },
+ });
+ await db.getRepository('collections').create({
+ values: {
+ name: 'projects',
+ fields: [
+ {
+ name: 'id',
+ type: 'bigInt',
+ autoIncrement: true,
+ primaryKey: true,
+ allowNull: false,
+ },
+ {
+ name: 'title',
+ type: 'string',
+ },
+ {
+ name: 'users',
+ type: 'belongsTo',
+ foreignKey: 'user_id',
+ target: 'users',
+ },
+ ],
+ },
+ });
+ // @ts-ignore
+ await db.getRepository('collections').load();
+ await db.sync();
+ await db.getRepository('tags').create({
+ values: [{ title: 'a' }, { title: 'b' }, { title: 'c' }],
+ });
+ await db.getRepository('users').create({
+ values: { id: 1, username: 'a' },
+ });
+ await db.getRepository('projects').create({
+ values: { id: 1, title: 'p1', user_id: 1 },
+ });
+ await expect(
+ db.getRepository('projects').findOne({
+ appends: ['users', 'users.tags'],
+ filter: {
+ $and: [
+ {
+ users: {
+ username: 'a',
+ },
+ },
+ ],
+ },
+ }),
+ ).resolves.toBeTruthy();
+ });
});
diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts
index e20b709e05..778c196582 100644
--- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts
+++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts
@@ -207,15 +207,8 @@ describe('m2m array api, bigInt targetKey', () => {
},
},
});
- if (db.sequelize.getDialect() === 'postgres') {
- const res = await search;
- expect(res.length).toBe(1);
- } else {
- expect(search).rejects.toThrowError();
- }
- if (db.sequelize.getDialect() !== 'postgres') {
- return;
- }
+ const res1 = await search;
+ expect(res1.length).toBe(1);
const search2 = db.getRepository('users').find({
filter: {
'tags.title': {
@@ -223,12 +216,8 @@ describe('m2m array api, bigInt targetKey', () => {
},
},
});
- if (db.sequelize.getDialect() === 'postgres') {
- const res = await search2;
- expect(res.length).toBe(2);
- } else {
- expect(search2).rejects.toThrowError();
- }
+ const res2 = await search2;
+ expect(res2.length).toBe(2);
});
it('should create with belongsToArray', async () => {
diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-string-api.test.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-string-api.test.ts
index 3d1f1d8265..2c0cf26e4b 100644
--- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-string-api.test.ts
+++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-string-api.test.ts
@@ -186,15 +186,8 @@ describe('m2m array api, string targetKey', () => {
},
},
});
- if (db.sequelize.getDialect() === 'postgres') {
- const res = await search;
- expect(res.length).toBe(1);
- } else {
- expect(search).rejects.toThrowError();
- }
- if (db.sequelize.getDialect() !== 'postgres') {
- return;
- }
+ const res1 = await search;
+ expect(res1.length).toBe(1);
const search2 = db.getRepository('users').find({
filter: {
'tags.title': {
@@ -202,12 +195,8 @@ describe('m2m array api, string targetKey', () => {
},
},
});
- if (db.sequelize.getDialect() === 'postgres') {
- const res = await search2;
- expect(res.length).toBe(2);
- } else {
- expect(search2).rejects.toThrowError();
- }
+ const res2 = await search2;
+ expect(res2.length).toBe(2);
});
it('should create with belongsToArray', async () => {
diff --git a/packages/plugins/@nocobase/plugin-field-markdown-vditor/package.json b/packages/plugins/@nocobase/plugin-field-markdown-vditor/package.json
index bbd35c7580..516562a881 100644
--- a/packages/plugins/@nocobase/plugin-field-markdown-vditor/package.json
+++ b/packages/plugins/@nocobase/plugin-field-markdown-vditor/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表字段:Markdown(Vditor)",
"description": "Used to store Markdown and render it using Vditor editor, supports common Markdown syntax such as list, code, quote, etc., and supports uploading images, recordings, etc.It also allows for instant rendering, where what you see is what you get.",
"description.zh-CN": "用于存储 Markdown,并使用 Vditor 编辑器渲染,支持常见 Markdown 语法,如列表,代码,引用等,并支持上传图片,录音等。同时可以做到即时渲染,所见即所得。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/field-markdown-vditor",
diff --git a/packages/plugins/@nocobase/plugin-field-sequence/package.json b/packages/plugins/@nocobase/plugin-field-sequence/package.json
index 8e7aa8e8b6..3f740cb220 100644
--- a/packages/plugins/@nocobase/plugin-field-sequence/package.json
+++ b/packages/plugins/@nocobase/plugin-field-sequence/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表字段:自动编码",
"description": "Automatically generate codes based on configured rules, supporting combinations of dates, numbers, and text.",
"description.zh-CN": "根据配置的规则自动生成编码,支持日期、数字、文本的组合。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/field-sequence",
diff --git a/packages/plugins/@nocobase/plugin-field-sort/package.json b/packages/plugins/@nocobase/plugin-field-sort/package.json
index f7bcf1cf1f..435cddc733 100644
--- a/packages/plugins/@nocobase/plugin-field-sort/package.json
+++ b/packages/plugins/@nocobase/plugin-field-sort/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-field-sort",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"displayName": "Collection field: Sort",
"displayName.zh-CN": "数据表字段:排序",
diff --git a/packages/plugins/@nocobase/plugin-file-manager/package.json b/packages/plugins/@nocobase/plugin-file-manager/package.json
index b8813dbf55..8ea463b760 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/package.json
+++ b/packages/plugins/@nocobase/plugin-file-manager/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-file-manager",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "File manager",
"displayName.zh-CN": "文件管理器",
"description": "Provides files storage services with files collection template and attachment field.",
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client/hooks/useUploadFiles.ts b/packages/plugins/@nocobase/plugin-file-manager/src/client/hooks/useUploadFiles.ts
index f30aadafbc..2b74a580ee 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/client/hooks/useUploadFiles.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/client/hooks/useUploadFiles.ts
@@ -20,7 +20,7 @@ import { useStorageUploadProps } from './useStorageUploadProps';
export const useUploadFiles = () => {
const { getDataBlockRequest } = useDataBlockRequestGetter();
- const { association } = useDataBlockProps();
+ const { association } = useDataBlockProps() || {};
const { setVisible } = useActionContext();
const collection = useCollection();
const sourceId = useSourceId();
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/storages.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/storages.ts
index bf66f7624a..20e248f469 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/storages.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/storages.ts
@@ -21,6 +21,7 @@ export default defineCollection({
type: 'string',
name: 'title',
translation: true,
+ trim: true,
},
{
title: '英文标识',
@@ -28,6 +29,7 @@ export default defineCollection({
type: 'uid',
name: 'name',
unique: true,
+ trim: true,
},
{
comment: '类型标识,如 local/ali-oss 等',
@@ -51,12 +53,14 @@ export default defineCollection({
type: 'text',
name: 'path',
defaultValue: '',
+ trim: true,
},
{
comment: '访问地址前缀',
type: 'string',
name: 'baseUrl',
defaultValue: '',
+ trim: true,
},
// TODO(feature): 需要使用一个实现了可设置默认值的字段
{
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/index.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/index.ts
index 10daee88fe..2390d1852d 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/index.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/index.ts
@@ -11,7 +11,7 @@ import { StorageEngine } from 'multer';
export * from '../constants';
export { AttachmentModel, default, PluginFileManagerServer, StorageModel } from './server';
-
+export { cloudFilenameGetter } from './utils';
export { StorageType } from './storages';
export { StorageEngine };
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts
index d581ba8772..f7903a5db9 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts
@@ -313,6 +313,27 @@ export class PluginFileManagerServer extends Plugin {
const storageType = this.storageTypes.get(storage.type);
return new storageType(storage).getFileURL(file, preview ? storage.options.thumbnailRule : '');
}
+ async isPublicAccessStorage(storageName) {
+ const storageRepository = this.db.getRepository('storages');
+ const storages = await storageRepository.findOne({
+ filter: { default: true },
+ });
+ let storage;
+ if (!storageName) {
+ storage = storages;
+ } else {
+ storage = await storageRepository.findOne({
+ filter: {
+ name: storageName,
+ },
+ });
+ }
+ storage = this.parseStorage(storage);
+ if (['local', 'ali-oss', 's3', 'tx-cos'].includes(storage.type)) {
+ return true;
+ }
+ return !!storage.options?.public;
+ }
}
export default PluginFileManagerServer;
diff --git a/packages/plugins/@nocobase/plugin-gantt/package.json b/packages/plugins/@nocobase/plugin-gantt/package.json
index 4bd4f9054e..241e270f0c 100644
--- a/packages/plugins/@nocobase/plugin-gantt/package.json
+++ b/packages/plugins/@nocobase/plugin-gantt/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-gantt",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "Block: Gantt",
"displayName.zh-CN": "区块:甘特图",
"description": "Provides Gantt block.",
diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockProvider.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockProvider.tsx
index 64e413d2b7..d0fabdab40 100644
--- a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockProvider.tsx
@@ -104,10 +104,6 @@ export const GanttBlockProvider = (props) => {
const collection = cm.getCollection(props.collection, props.dataSource);
const params = { filter: props.params?.filter, paginate: false, sort: [collection?.primaryKey || 'id'] };
- if (collection?.tree) {
- params['tree'] = true;
- }
-
return (
diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/package.json b/packages/plugins/@nocobase/plugin-graph-collection-manager/package.json
index 7e5af12047..2ce1dff039 100644
--- a/packages/plugins/@nocobase/plugin-graph-collection-manager/package.json
+++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "可视化数据表管理",
"description": "An ER diagram-like tool. Currently only the Master database is supported.",
"description.zh-CN": "类似 ER 图的工具,目前只支持主数据库。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/graph-collection-manager",
diff --git a/packages/plugins/@nocobase/plugin-kanban/package.json b/packages/plugins/@nocobase/plugin-kanban/package.json
index 289f5d5b13..0acd5c8217 100644
--- a/packages/plugins/@nocobase/plugin-kanban/package.json
+++ b/packages/plugins/@nocobase/plugin-kanban/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-kanban",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/block-kanban",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/block-kanban",
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/.npmignore b/packages/plugins/@nocobase/plugin-locale-tester/.npmignore
new file mode 100644
index 0000000000..65f5e8779f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/.npmignore
@@ -0,0 +1,2 @@
+/node_modules
+/src
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/README.md b/packages/plugins/@nocobase/plugin-locale-tester/README.md
new file mode 100644
index 0000000000..cfac872027
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/README.md
@@ -0,0 +1 @@
+# @nocobase/plugin-locale-tester
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/client.d.ts b/packages/plugins/@nocobase/plugin-locale-tester/client.d.ts
new file mode 100644
index 0000000000..6c459cbac4
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/client.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/client';
+export { default } from './dist/client';
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/client.js b/packages/plugins/@nocobase/plugin-locale-tester/client.js
new file mode 100644
index 0000000000..b6e3be70e6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/client.js
@@ -0,0 +1 @@
+module.exports = require('./dist/client/index.js');
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/package.json b/packages/plugins/@nocobase/plugin-locale-tester/package.json
new file mode 100644
index 0000000000..327a32c4c6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@nocobase/plugin-locale-tester",
+ "displayName": "Locale tester",
+ "displayName.zh-CN": "翻译测试工具",
+ "version": "1.7.0-alpha.11",
+ "homepage": "https://github.com/nocobase/locales",
+ "main": "dist/server/index.js",
+ "peerDependencies": {
+ "@nocobase/client": "1.x",
+ "@nocobase/server": "1.x",
+ "@nocobase/test": "1.x"
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/server.d.ts b/packages/plugins/@nocobase/plugin-locale-tester/server.d.ts
new file mode 100644
index 0000000000..c41081ddc6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/server.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/server';
+export { default } from './dist/server';
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/server.js b/packages/plugins/@nocobase/plugin-locale-tester/server.js
new file mode 100644
index 0000000000..972842039a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/server.js
@@ -0,0 +1 @@
+module.exports = require('./dist/server/index.js');
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/client/client.d.ts b/packages/plugins/@nocobase/plugin-locale-tester/src/client/client.d.ts
new file mode 100644
index 0000000000..4e96f83fa1
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/src/client/client.d.ts
@@ -0,0 +1,249 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+// CSS modules
+type CSSModuleClasses = { readonly [key: string]: string };
+
+declare module '*.module.css' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.scss' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.sass' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.less' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.styl' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.stylus' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.pcss' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+declare module '*.module.sss' {
+ const classes: CSSModuleClasses;
+ export default classes;
+}
+
+// CSS
+declare module '*.css' { }
+declare module '*.scss' { }
+declare module '*.sass' { }
+declare module '*.less' { }
+declare module '*.styl' { }
+declare module '*.stylus' { }
+declare module '*.pcss' { }
+declare module '*.sss' { }
+
+// Built-in asset types
+// see `src/node/constants.ts`
+
+// images
+declare module '*.apng' {
+ const src: string;
+ export default src;
+}
+declare module '*.png' {
+ const src: string;
+ export default src;
+}
+declare module '*.jpg' {
+ const src: string;
+ export default src;
+}
+declare module '*.jpeg' {
+ const src: string;
+ export default src;
+}
+declare module '*.jfif' {
+ const src: string;
+ export default src;
+}
+declare module '*.pjpeg' {
+ const src: string;
+ export default src;
+}
+declare module '*.pjp' {
+ const src: string;
+ export default src;
+}
+declare module '*.gif' {
+ const src: string;
+ export default src;
+}
+declare module '*.svg' {
+ const src: string;
+ export default src;
+}
+declare module '*.ico' {
+ const src: string;
+ export default src;
+}
+declare module '*.webp' {
+ const src: string;
+ export default src;
+}
+declare module '*.avif' {
+ const src: string;
+ export default src;
+}
+
+// media
+declare module '*.mp4' {
+ const src: string;
+ export default src;
+}
+declare module '*.webm' {
+ const src: string;
+ export default src;
+}
+declare module '*.ogg' {
+ const src: string;
+ export default src;
+}
+declare module '*.mp3' {
+ const src: string;
+ export default src;
+}
+declare module '*.wav' {
+ const src: string;
+ export default src;
+}
+declare module '*.flac' {
+ const src: string;
+ export default src;
+}
+declare module '*.aac' {
+ const src: string;
+ export default src;
+}
+declare module '*.opus' {
+ const src: string;
+ export default src;
+}
+declare module '*.mov' {
+ const src: string;
+ export default src;
+}
+declare module '*.m4a' {
+ const src: string;
+ export default src;
+}
+declare module '*.vtt' {
+ const src: string;
+ export default src;
+}
+
+// fonts
+declare module '*.woff' {
+ const src: string;
+ export default src;
+}
+declare module '*.woff2' {
+ const src: string;
+ export default src;
+}
+declare module '*.eot' {
+ const src: string;
+ export default src;
+}
+declare module '*.ttf' {
+ const src: string;
+ export default src;
+}
+declare module '*.otf' {
+ const src: string;
+ export default src;
+}
+
+// other
+declare module '*.webmanifest' {
+ const src: string;
+ export default src;
+}
+declare module '*.pdf' {
+ const src: string;
+ export default src;
+}
+declare module '*.txt' {
+ const src: string;
+ export default src;
+}
+
+// wasm?init
+declare module '*.wasm?init' {
+ const initWasm: (options?: WebAssembly.Imports) => Promise;
+ export default initWasm;
+}
+
+// web worker
+declare module '*?worker' {
+ const workerConstructor: {
+ new(options?: { name?: string }): Worker;
+ };
+ export default workerConstructor;
+}
+
+declare module '*?worker&inline' {
+ const workerConstructor: {
+ new(options?: { name?: string }): Worker;
+ };
+ export default workerConstructor;
+}
+
+declare module '*?worker&url' {
+ const src: string;
+ export default src;
+}
+
+declare module '*?sharedworker' {
+ const sharedWorkerConstructor: {
+ new(options?: { name?: string }): SharedWorker;
+ };
+ export default sharedWorkerConstructor;
+}
+
+declare module '*?sharedworker&inline' {
+ const sharedWorkerConstructor: {
+ new(options?: { name?: string }): SharedWorker;
+ };
+ export default sharedWorkerConstructor;
+}
+
+declare module '*?sharedworker&url' {
+ const src: string;
+ export default src;
+}
+
+declare module '*?raw' {
+ const src: string;
+ export default src;
+}
+
+declare module '*?url' {
+ const src: string;
+ export default src;
+}
+
+declare module '*?inline' {
+ const src: string;
+ export default src;
+}
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/client/index.tsx b/packages/plugins/@nocobase/plugin-locale-tester/src/client/index.tsx
new file mode 100644
index 0000000000..7ecfc9a69d
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/src/client/index.tsx
@@ -0,0 +1,124 @@
+/**
+ * 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 { useForm } from '@formily/react';
+import { ActionProps, ISchema, Plugin, SchemaComponent, useAPIClient, useApp, useRequest } from '@nocobase/client';
+import { Alert, App as AntdApp, Card, Spin } from 'antd';
+import React from 'react';
+import { useT } from './locale';
+
+function LocaleTester() {
+ const { data, loading } = useRequest({
+ url: 'localeTester:get',
+ });
+ const t = useT();
+
+ const schema: ISchema = {
+ type: 'void',
+ name: 'root',
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'FormV2',
+ properties: {
+ locale: {
+ type: 'string',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input.JSON',
+ 'x-component-props': {
+ autoSize: { minRows: 20, maxRows: 30 },
+ },
+ default: data?.data?.locale,
+ title: t('Translations'),
+ },
+ button: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: t('Submit'),
+ 'x-use-component-props': 'useSubmitActionProps',
+ },
+ },
+ },
+ },
+ };
+
+ const useSubmitActionProps = () => {
+ const form = useForm();
+ const api = useAPIClient();
+ const { message } = AntdApp.useApp();
+ const app = useApp();
+
+ return {
+ type: 'primary',
+ htmlType: 'submit',
+ async onClick() {
+ await form.submit();
+ const values = form.values;
+ await api.request({
+ url: 'localeTester:updateOrCreate',
+ method: 'post',
+ params: {
+ filterKeys: ['id'],
+ },
+ data: {
+ id: data?.data?.id,
+ locale: values.locale,
+ },
+ });
+ message.success(app.i18n.t('Saved successfully!'));
+ window.location.reload();
+ },
+ };
+ };
+
+ if (loading) {
+ return ;
+ }
+ return (
+
+ nocobase/locales to get the language file that needs translation, then paste it below and provide the translation.`,
+ ),
+ }}
+ >
+ }
+ />
+
+
+ );
+}
+
+export class PluginLocaleTesterClient extends Plugin {
+ async afterAdd() {
+ // await this.app.pm.add()
+ }
+
+ async beforeLoad() {}
+
+ // You can get and modify the app instance here
+ async load() {
+ this.app.pluginSettingsManager.add('locale-tester', {
+ title: this.t('Locale tester'),
+ icon: 'TranslationOutlined',
+ Component: LocaleTester,
+ });
+ // this.app.addComponents({})
+ // this.app.addScopes({})
+ // this.app.addProvider()
+ // this.app.addProviders()
+ // this.app.router.add()
+ }
+}
+
+export default PluginLocaleTesterClient;
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/client/locale.ts b/packages/plugins/@nocobase/plugin-locale-tester/src/client/locale.ts
new file mode 100644
index 0000000000..84797b7d1b
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/src/client/locale.ts
@@ -0,0 +1,21 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+// @ts-ignore
+import pkg from './../../package.json';
+import { useApp } from '@nocobase/client';
+
+export function useT() {
+ const app = useApp();
+ return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] });
+}
+
+export function tStr(key: string) {
+ return `{{t(${JSON.stringify(key)}, { ns: ['${pkg.name}', 'client'], nsMode: 'fallback' })}}`;
+}
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/index.ts b/packages/plugins/@nocobase/plugin-locale-tester/src/index.ts
new file mode 100644
index 0000000000..be99a2ff1a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/src/index.ts
@@ -0,0 +1,11 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+export * from './server';
+export { default } from './server';
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-locale-tester/src/locale/en-US.json
new file mode 100644
index 0000000000..cc8bcdd470
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/src/locale/en-US.json
@@ -0,0 +1,5 @@
+{
+ "Locale": "Locale",
+ "Locale tester": "Locale tester",
+ "Please go to nocobase/locales to get the language file that needs translation, then paste it below and provide the translation.": "Please go to nocobase/locales to get the language file that needs translation, then paste it below and provide the translation."
+}
\ No newline at end of file
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-locale-tester/src/locale/zh-CN.json
new file mode 100644
index 0000000000..d4074a2ae0
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/src/locale/zh-CN.json
@@ -0,0 +1,5 @@
+{
+ "Translations": "翻译",
+ "Locale tester": "翻译测试工具",
+ "Please go to nocobase/locales to get the language file that needs translation, then paste it below and provide the translation.": "请前往 nocobase/locales 获取需要翻译的语言文件,粘贴到下方并进行翻译。"
+}
\ No newline at end of file
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/server/collections/.gitkeep b/packages/plugins/@nocobase/plugin-locale-tester/src/server/collections/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/server/collections/localeTester.ts b/packages/plugins/@nocobase/plugin-locale-tester/src/server/collections/localeTester.ts
new file mode 100644
index 0000000000..eceb54bbbc
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/src/server/collections/localeTester.ts
@@ -0,0 +1,21 @@
+/**
+ * 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 { defineCollection } from '@nocobase/database';
+
+export default defineCollection({
+ name: 'localeTester',
+ autoGenId: true,
+ fields: [
+ {
+ type: 'json',
+ name: 'locale',
+ },
+ ],
+});
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/server/index.ts b/packages/plugins/@nocobase/plugin-locale-tester/src/server/index.ts
new file mode 100644
index 0000000000..be989de7c3
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/src/server/index.ts
@@ -0,0 +1,10 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+export { default } from './plugin';
diff --git a/packages/plugins/@nocobase/plugin-locale-tester/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-locale-tester/src/server/plugin.ts
new file mode 100644
index 0000000000..7c9367961a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-locale-tester/src/server/plugin.ts
@@ -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.
+ */
+
+import { Plugin } from '@nocobase/server';
+import _ from 'lodash';
+
+export class PluginLocaleTesterServer extends Plugin {
+ async afterAdd() {}
+
+ async beforeLoad() {
+ this.app.acl.registerSnippet({
+ name: `pm.${this.name}`,
+ actions: ['localeTester:*'],
+ });
+ }
+
+ async load() {
+ this.app.resourceManager.use(async (ctx, next) => {
+ await next();
+ const { resourceName, actionName } = ctx.action;
+ if (resourceName === 'app' && actionName === 'getLang') {
+ const repository = this.db.getRepository('localeTester');
+ const record = await repository.findOne();
+ const locale = record?.locale || {};
+ if (locale['cronstrue']) {
+ _.set(ctx.body, 'cronstrue', locale['cronstrue']);
+ }
+ if (locale['react-js-cron']) {
+ _.set(ctx.body, 'cron', locale['react-js-cron']);
+ }
+ Object.keys(locale).forEach((key) => {
+ if (key === 'cronstrue' || key === 'react-js-cron') {
+ return;
+ }
+ const value = locale[key];
+ _.set(ctx.body, ['resources', key], value);
+ const k = key.replace('@nocobase/', '').replace('@nocobase/plugin-', '');
+ _.set(ctx.body, ['resources', k], value);
+ });
+ }
+ });
+ }
+
+ async install() {}
+
+ async afterEnable() {}
+
+ async afterDisable() {}
+
+ async remove() {}
+}
+
+export default PluginLocaleTesterServer;
diff --git a/packages/plugins/@nocobase/plugin-localization/package.json b/packages/plugins/@nocobase/plugin-localization/package.json
index 405744fda8..30eb37c08a 100644
--- a/packages/plugins/@nocobase/plugin-localization/package.json
+++ b/packages/plugins/@nocobase/plugin-localization/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-localization",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/localization-management",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/localization-management",
diff --git a/packages/plugins/@nocobase/plugin-localization/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-localization/src/server/plugin.ts
index 7bac0fcf63..8d04cac1b3 100644
--- a/packages/plugins/@nocobase/plugin-localization/src/server/plugin.ts
+++ b/packages/plugins/@nocobase/plugin-localization/src/server/plugin.ts
@@ -25,7 +25,7 @@ export class PluginLocalizationServer extends Plugin {
addNewTexts = async (texts: { text: string; module: string }[], options?: any) => {
texts = await this.resources.filterExists(texts, options?.transaction);
- this.db
+ await this.db
.getModel('localizationTexts')
.bulkCreate(
texts.map(({ text, module }) => ({
diff --git a/packages/plugins/@nocobase/plugin-logger/package.json b/packages/plugins/@nocobase/plugin-logger/package.json
index e280aac447..20116132a2 100644
--- a/packages/plugins/@nocobase/plugin-logger/package.json
+++ b/packages/plugins/@nocobase/plugin-logger/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "日志",
"description": "Server-side logs, mainly including API request logs and system runtime logs, and allows to package and download log files.",
"description.zh-CN": "服务端日志,主要包括接口请求日志和系统运行日志,并支持打包和下载日志文件。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/logger",
diff --git a/packages/plugins/@nocobase/plugin-map/package.json b/packages/plugins/@nocobase/plugin-map/package.json
index e42ea56ede..43e2951ff3 100644
--- a/packages/plugins/@nocobase/plugin-map/package.json
+++ b/packages/plugins/@nocobase/plugin-map/package.json
@@ -2,7 +2,7 @@
"name": "@nocobase/plugin-map",
"displayName": "Block: Map",
"displayName.zh-CN": "区块:地图",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"description": "Map block, support Gaode map and Google map, you can also extend more map types.",
"description.zh-CN": "地图区块,支持高德地图和 Google 地图,你也可以扩展更多地图类型。",
"license": "AGPL-3.0",
diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/Configuration.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/Configuration.tsx
index ab8af6f8bb..168cdb220c 100644
--- a/packages/plugins/@nocobase/plugin-map/src/client/components/Configuration.tsx
+++ b/packages/plugins/@nocobase/plugin-map/src/client/components/Configuration.tsx
@@ -32,15 +32,26 @@ const BaseConfiguration: React.FC = ({ type, children })
return apiClient.resource(MapConfigurationResourceKey);
}, [apiClient]);
- function removeInvisibleCharsFromObject(obj: Record): Record {
- const cleanObj: Record = {};
+ function removeInvisibleCharsFromObject(obj: Record): Record {
+ const cleanObj: Record = {};
for (const [key, value] of Object.entries(obj)) {
- cleanObj[key] = typeof value === 'string' ? value.replace(/[\p{C}\p{Z}\p{Zl}\p{Zp}]+/gu, '') : value;
+ if (typeof value === 'string') {
+ // 去除不可见字符
+ const cleanedValue = value.replace(/[\p{C}\p{Z}\p{Zl}\p{Zp}]+/gu, '');
+ // 如果清理后为空字符串,则赋值为 null
+ cleanObj[key] = cleanedValue || null;
+ }
}
+
return cleanObj;
}
-
const onSubmit = async (values) => {
+ // 移除不可见字符并更新表单值
+ const result = removeInvisibleCharsFromObject(values);
+ form.setFieldsValue(result);
+
+ // 等待表单值更新完成后再校验
+ await new Promise((resolve) => setTimeout(resolve, 0));
await form.validateFields();
resource
.set({
diff --git a/packages/plugins/@nocobase/plugin-mobile-client/package.json b/packages/plugins/@nocobase/plugin-mobile-client/package.json
index 7cc94e26d9..33853bca25 100644
--- a/packages/plugins/@nocobase/plugin-mobile-client/package.json
+++ b/packages/plugins/@nocobase/plugin-mobile-client/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-mobile-client",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/mobile-client",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/mobile-client",
diff --git a/packages/plugins/@nocobase/plugin-mobile/package.json b/packages/plugins/@nocobase/plugin-mobile/package.json
index 927fd1c3c0..af3767a3ee 100644
--- a/packages/plugins/@nocobase/plugin-mobile/package.json
+++ b/packages/plugins/@nocobase/plugin-mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-mobile",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/mobile",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/mobile",
diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/ShowTipWhenNoPages.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/ShowTipWhenNoPages.tsx
index aaeaf140fc..2233d105ba 100644
--- a/packages/plugins/@nocobase/plugin-mobile/src/client/ShowTipWhenNoPages.tsx
+++ b/packages/plugins/@nocobase/plugin-mobile/src/client/ShowTipWhenNoPages.tsx
@@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
+import { HighlightOutlined } from '@ant-design/icons';
import { useDesignable } from '@nocobase/client';
import { ErrorBlock } from 'antd-mobile';
import _ from 'lodash';
@@ -24,10 +25,10 @@ export const ShowTipWhenNoPages: FC = ({ children }) => {
if (!isInBlockTemplateSettingsPage && (!designable || isMobile) && _.isEmpty(routeList)) {
return (
}
fullPage
- title={t('No accessible pages found')}
- description={t('This might be due to permission configuration issues')}
+ title={t('No pages yet, please configure first')}
+ description={t('Click the "UI Editor" icon in the upper right corner to enter the UI Editor mode')}
/>
);
}
diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.style.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.style.ts
index e4c3540950..83b46cb8ad 100644
--- a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.style.ts
+++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.style.ts
@@ -21,10 +21,6 @@ export const useMobileActionPageStyle = genStyleHook('nb-mobile-action-page', (t
bottom: 0,
backgroundColor: token.colorBgLayout,
- '.mobile-page-content > .nb-grid-container > .nb-grid > .nb-grid-warp > .ant-btn': {
- margin: 20,
- },
-
'.nb-mobile-action-page-footer': {
height: 'var(--nb-mobile-page-header-height)',
paddingRight: 'var(--nb-mobile-page-tabs-content-padding)',
diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/desktop-mode/Header.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/desktop-mode/Header.tsx
index 2fe7aa4cfc..c7ad902bad 100644
--- a/packages/plugins/@nocobase/plugin-mobile/src/client/desktop-mode/Header.tsx
+++ b/packages/plugins/@nocobase/plugin-mobile/src/client/desktop-mode/Header.tsx
@@ -98,8 +98,8 @@ export const DesktopModeHeader: FC = () => {
{t('Back')}
-
- {allowConfigUI ?
: null}
+
+ {allowConfigUI ?
: null}
{
setSize({ width: 768, height: 667 });
diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/styles.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/styles.ts
index 434308bcb4..db2cf1bbaf 100644
--- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/styles.ts
+++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/styles.ts
@@ -28,6 +28,7 @@ export const useStyles = genStyleHook('nb-mobile-tab-bar', (token) => {
alignItems: 'center',
gap: '1em',
height: '100%',
+ minHeight: 49,
},
'.mobile-tab-bar-list': {
diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx
index 8f08e1fddf..0c1e1630a4 100644
--- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx
+++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx
@@ -9,6 +9,7 @@
import {
AdminProvider,
+ AllDataBlocksProvider,
AntdAppProvider,
AssociationFieldMode,
AssociationFieldModeProvider,
@@ -126,7 +127,9 @@ export const Mobile = () => {
{/* the z-index of all popups and subpages will be based on this value */}
-
+
+
+
diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/components/MobilePicker.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/components/MobilePicker.tsx
index 2ff9153b2c..4a3f4a43fb 100644
--- a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/components/MobilePicker.tsx
+++ b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/components/MobilePicker.tsx
@@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
const MobilePicker = connect(
(props) => {
const { value, onChange, disabled, options = [], mode } = props;
+ console.log(props);
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [selected, setSelected] = useState(value || []);
@@ -42,7 +43,7 @@ const MobilePicker = connect(
disabled={disabled}
value={value}
dropdownStyle={{ display: 'none' }}
- multiple={mode === 'multiple'}
+ multiple={['multiple', 'tags'].includes(mode)}
onClear={() => {
setVisible(false);
onChange(null);
@@ -77,10 +78,10 @@ const MobilePicker = connect(
}}
>
{
- if (mode === 'multiple') {
+ if (['multiple', 'tags'].includes(mode)) {
setSelected(val);
} else {
setSelected(val[0]);
@@ -96,7 +97,7 @@ const MobilePicker = connect(
))}
- {mode === 'multiple' && (
+ {['multiple', 'tags'].includes(mode) && (
{t('Confirm')}
diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/initializer.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/initializer.tsx
index 8026ff27ab..af89b5eb2b 100644
--- a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/initializer.tsx
+++ b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/initializer.tsx
@@ -15,9 +15,6 @@ export const mobileAddBlockInitializer = new SchemaInitializer({
name: 'mobile:addBlock',
icon: 'PlusOutlined',
wrap: gridRowColWrap,
- style: {
- margin: 20,
- },
items: [
{
name: 'dataBlocks',
diff --git a/packages/plugins/@nocobase/plugin-mobile/src/server/__tests__/union-role-mobileRoutes.test.ts b/packages/plugins/@nocobase/plugin-mobile/src/server/__tests__/union-role-mobileRoutes.test.ts
index b1930b8629..a66cbc73a4 100644
--- a/packages/plugins/@nocobase/plugin-mobile/src/server/__tests__/union-role-mobileRoutes.test.ts
+++ b/packages/plugins/@nocobase/plugin-mobile/src/server/__tests__/union-role-mobileRoutes.test.ts
@@ -54,7 +54,11 @@ describe('union role mobileRoutes', async () => {
roles: [role1.name, role2.name],
},
});
-
+ await rootAgent.resource('roles').setSystemRoleMode({
+ values: {
+ roleMode: SystemRoleMode.allowUseUnion,
+ },
+ });
agent = await app.agent().login(user, UNION_ROLE_KEY);
});
diff --git a/packages/plugins/@nocobase/plugin-mock-collections/package.json b/packages/plugins/@nocobase/plugin-mock-collections/package.json
index 599dab8ed7..62f74c6994 100644
--- a/packages/plugins/@nocobase/plugin-mock-collections/package.json
+++ b/packages/plugins/@nocobase/plugin-mock-collections/package.json
@@ -2,7 +2,7 @@
"name": "@nocobase/plugin-mock-collections",
"displayName": "mock-collections",
"description": "mock-collections",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"license": "AGPL-3.0",
"peerDependencies": {
diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/package.json b/packages/plugins/@nocobase/plugin-multi-app-manager/package.json
index 95acdb23d1..2030a90e2a 100644
--- a/packages/plugins/@nocobase/plugin-multi-app-manager/package.json
+++ b/packages/plugins/@nocobase/plugin-multi-app-manager/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "多应用管理器",
"description": "Dynamically create multiple apps without separate deployments.",
"description.zh-CN": "无需单独部署即可动态创建多个应用。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/multi-app-manager",
diff --git a/packages/plugins/@nocobase/plugin-multi-app-share-collection/package.json b/packages/plugins/@nocobase/plugin-multi-app-share-collection/package.json
index 596128a6cc..71bc9c4d28 100644
--- a/packages/plugins/@nocobase/plugin-multi-app-share-collection/package.json
+++ b/packages/plugins/@nocobase/plugin-multi-app-share-collection/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "多应用数据表共享",
"description": "",
"description.zh-CN": "",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"devDependencies": {
"@formily/react": "2.x",
diff --git a/packages/plugins/@nocobase/plugin-notification-email/package.json b/packages/plugins/@nocobase/plugin-notification-email/package.json
index 867fe8a98b..5254afa5d6 100644
--- a/packages/plugins/@nocobase/plugin-notification-email/package.json
+++ b/packages/plugins/@nocobase/plugin-notification-email/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-notification-email",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "Notification: Email",
"displayName.zh-CN": "通知:电子邮件",
"description": "Used for sending email notifications with built-in SMTP transport.",
diff --git a/packages/plugins/@nocobase/plugin-notification-in-app-message/package.json b/packages/plugins/@nocobase/plugin-notification-in-app-message/package.json
index e92083b5cb..b999b960c6 100644
--- a/packages/plugins/@nocobase/plugin-notification-in-app-message/package.json
+++ b/packages/plugins/@nocobase/plugin-notification-in-app-message/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-notification-in-app-message",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"displayName": "Notification: In-app message",
"displayName.zh-CN": "通知:站内信",
"description": "It supports users in receiving real-time message notifications within the NocoBase application.",
diff --git a/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MessagePage.tsx b/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MessagePage.tsx
index d5926115a9..f6c6cec992 100644
--- a/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MessagePage.tsx
+++ b/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MessagePage.tsx
@@ -30,8 +30,16 @@ import {
} from '../../observables';
import InfiniteScrollContent from './InfiniteScrollContent';
+function removeStringIfStartsWith(text: string, prefix: string): string {
+ if (text.startsWith(prefix)) {
+ return text.slice(prefix.length);
+ }
+ return text;
+}
+
const MobileMessagePageInner = (props: { displayPageHeader?: boolean }) => {
const app = useApp();
+ const basename = app.router.basename.replace(/\/+$/, '');
const { t } = useLocalTranslation();
const navigate = useNavigate();
const ctx = useCurrentUserContext();
@@ -57,7 +65,7 @@ const MobileMessagePageInner = (props: { displayPageHeader?: boolean }) => {
if (url) {
if (url.startsWith('/m/')) navigate(url.substring(2));
else if (url.startsWith('/')) {
- navigate(url);
+ navigate(removeStringIfStartsWith(url, basename));
inboxVisible.value = false;
} else {
window.location.href = url;
diff --git a/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MobileTabBarMessageItem.tsx b/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MobileTabBarMessageItem.tsx
index 8531d4a1aa..4233489589 100644
--- a/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MobileTabBarMessageItem.tsx
+++ b/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MobileTabBarMessageItem.tsx
@@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import React, { useEffect } from 'react';
import { observer } from '@formily/reactive-react';
-import { useNavigate, useLocation } from 'react-router-dom';
import { MobileTabBarItem } from '@nocobase/plugin-mobile/client';
-import { unreadMsgsCountObs, startMsgSSEStreamWithRetry, updateUnreadMsgsCount } from '../../observables';
+import React, { useEffect } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
+import { startMsgSSEStreamWithRetry, unreadMsgsCountObs, updateUnreadMsgsCount } from '../../observables';
const InnerMobileTabBarMessageItem = (props) => {
const navigate = useNavigate();
@@ -19,9 +19,32 @@ const InnerMobileTabBarMessageItem = (props) => {
const onClick = () => {
navigate('/page/in-app-message');
};
+
useEffect(() => {
- startMsgSSEStreamWithRetry();
+ const disposes: Array<() => void> = [];
+ disposes.push(startMsgSSEStreamWithRetry());
+ const disposeAll = () => {
+ while (disposes.length > 0) {
+ const dispose = disposes.pop();
+ dispose && dispose();
+ }
+ };
+
+ const onVisibilityChange = () => {
+ if (document.visibilityState === 'visible') {
+ disposes.push(startMsgSSEStreamWithRetry());
+ } else {
+ disposeAll();
+ }
+ };
+
+ document.addEventListener('visibilitychange', onVisibilityChange);
+ return () => {
+ disposeAll();
+ document.removeEventListener('visibilitychange', onVisibilityChange);
+ };
}, []);
+
const selected = props.url && location.pathname.startsWith(props.url);
return (
diff --git a/packages/plugins/@nocobase/plugin-notification-manager/package.json b/packages/plugins/@nocobase/plugin-notification-manager/package.json
index ac6e05d1f7..770d91fe31 100644
--- a/packages/plugins/@nocobase/plugin-notification-manager/package.json
+++ b/packages/plugins/@nocobase/plugin-notification-manager/package.json
@@ -4,7 +4,7 @@
"description": "Provides a unified management service that includes channel configuration, logging, and other features, supporting the configuration of various notification channels, including in-app message and email.",
"displayName.zh-CN": "通知管理",
"description.zh-CN": "提供统一的管理服务,涵盖渠道配置、日志记录等功能,支持多种通知渠道的配置,包括站内信和电子邮件等。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"homepage": "https://docs.nocobase.com/handbook/notification-manager",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/notification-manager",
"main": "dist/server/index.js",
diff --git a/packages/plugins/@nocobase/plugin-notifications/package.json b/packages/plugins/@nocobase/plugin-notifications/package.json
index a935a9fba8..e9fb5d6b5a 100644
--- a/packages/plugins/@nocobase/plugin-notifications/package.json
+++ b/packages/plugins/@nocobase/plugin-notifications/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-notifications",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"description": "",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
diff --git a/packages/plugins/@nocobase/plugin-public-forms/package.json b/packages/plugins/@nocobase/plugin-public-forms/package.json
index ff9f36e161..9a1d24fdfb 100644
--- a/packages/plugins/@nocobase/plugin-public-forms/package.json
+++ b/packages/plugins/@nocobase/plugin-public-forms/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-public-forms",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"displayName": "Public forms",
"displayName.zh-CN": "公开表单",
diff --git a/packages/plugins/@nocobase/plugin-public-forms/src/client/components/AdminPublicFormPage.tsx b/packages/plugins/@nocobase/plugin-public-forms/src/client/components/AdminPublicFormPage.tsx
index a9fe6f7ee5..b62b6e8b29 100644
--- a/packages/plugins/@nocobase/plugin-public-forms/src/client/components/AdminPublicFormPage.tsx
+++ b/packages/plugins/@nocobase/plugin-public-forms/src/client/components/AdminPublicFormPage.tsx
@@ -147,6 +147,7 @@ export function AdminPublicFormPage() {
}}
>
{t('Public forms', { ns: NAMESPACE })},
diff --git a/packages/plugins/@nocobase/plugin-public-forms/src/client/components/PublicFormPage.tsx b/packages/plugins/@nocobase/plugin-public-forms/src/client/components/PublicFormPage.tsx
index 3e8635b3dd..f9af7fba98 100644
--- a/packages/plugins/@nocobase/plugin-public-forms/src/client/components/PublicFormPage.tsx
+++ b/packages/plugins/@nocobase/plugin-public-forms/src/client/components/PublicFormPage.tsx
@@ -33,7 +33,7 @@ import {
import { Input, Modal, Spin } from 'antd';
import { useTranslation } from 'react-i18next';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
-import { isDesktop } from 'react-device-detect';
+import { isDesktop, isMobile } from 'react-device-detect';
import { useParams } from 'react-router';
import { usePublicSubmitActionProps } from '../hooks';
import { UnEnabledFormPlaceholder, UnFoundFormPlaceholder } from './UnEnabledFormPlaceholder';
@@ -129,9 +129,6 @@ const PublicFormMessageProvider = ({ children }) => {
);
};
-function isMobile() {
- return window.matchMedia('(max-width: 768px)').matches;
-}
const AssociationFieldMobile = (props) => {
return ;
@@ -165,7 +162,6 @@ const mobileComponents = {
function InternalPublicForm() {
const params = useParams();
const apiClient = useAPIClient();
- const isMobileMedia = isMobile();
const { error, data, loading, run } = useRequest(
{
url: `publicForms:getMeta/${params.name}`,
@@ -243,7 +239,7 @@ function InternalPublicForm() {
if (!data?.data) {
return ;
}
- const components = isMobileMedia ? mobileComponents : {};
+ const components = isMobile ? mobileComponents : {};
return (
diff --git a/packages/plugins/@nocobase/plugin-public-forms/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-public-forms/src/server/plugin.ts
index 54bf5bcff4..c5611799f3 100644
--- a/packages/plugins/@nocobase/plugin-public-forms/src/server/plugin.ts
+++ b/packages/plugins/@nocobase/plugin-public-forms/src/server/plugin.ts
@@ -172,7 +172,7 @@ export class PluginPublicFormsServer extends Plugin {
skip: true,
};
} else if (
- (actionName === 'list' && ctx.PublicForm['targetCollections'].includes(resourceName)) ||
+ (['list', 'get'].includes(actionName) && ctx.PublicForm['targetCollections'].includes(resourceName)) ||
(collection?.options.template === 'file' && actionName === 'create') ||
(resourceName === 'storages' && ['getBasicInfo', 'createPresignedUrl'].includes(actionName)) ||
(resourceName === 'vditor' && ['check'].includes(actionName)) ||
diff --git a/packages/plugins/@nocobase/plugin-sample-hello/package.json b/packages/plugins/@nocobase/plugin-sample-hello/package.json
index f73c88e5dd..d6c79d85ee 100644
--- a/packages/plugins/@nocobase/plugin-sample-hello/package.json
+++ b/packages/plugins/@nocobase/plugin-sample-hello/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-sample-hello",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "./dist/server/index.js",
"displayName": "Hello",
"displayName.zh-CN": "Hello",
diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/package.json b/packages/plugins/@nocobase/plugin-snapshot-field/package.json
index a1440a740f..d6837aeb32 100644
--- a/packages/plugins/@nocobase/plugin-snapshot-field/package.json
+++ b/packages/plugins/@nocobase/plugin-snapshot-field/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表字段:关系快照",
"description": "When adding a new record, create a snapshot for its relational record and save in the new record. The snapshot will not be updated when the relational record is updated.",
"description.zh-CN": "在添加数据时,为它的关系数据创建快照,并保存在当前的数据中。关系数据更新时,快照不会更新。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/field-snapshot",
diff --git a/packages/plugins/@nocobase/plugin-system-settings/package.json b/packages/plugins/@nocobase/plugin-system-settings/package.json
index 6c64da8a3a..44f1467c0c 100644
--- a/packages/plugins/@nocobase/plugin-system-settings/package.json
+++ b/packages/plugins/@nocobase/plugin-system-settings/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "系统设置",
"description": "Used to adjust the system title, logo, language, etc.",
"description.zh-CN": "用于调整系统的标题、LOGO、语言等。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/system-settings",
diff --git a/packages/plugins/@nocobase/plugin-theme-editor/package.json b/packages/plugins/@nocobase/plugin-theme-editor/package.json
index 2ecc65a045..0edb35befb 100644
--- a/packages/plugins/@nocobase/plugin-theme-editor/package.json
+++ b/packages/plugins/@nocobase/plugin-theme-editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-theme-editor",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/theme-editor",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/theme-editor",
diff --git a/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/meta/category.ts b/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/meta/category.ts
index 3b2509f3ce..fd7ca4ad71 100644
--- a/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/meta/category.ts
+++ b/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/meta/category.ts
@@ -279,7 +279,7 @@ const category: TokenTree = [
nameEn: 'Others',
desc: '',
descEn: '',
- seedToken: ['wireframe'],
+ seedToken: ['wireframe', 'siderWidth'],
},
],
},
diff --git a/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/token-panel-pro/TokenContent.tsx b/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/token-panel-pro/TokenContent.tsx
index 86f129146d..88b89eb58b 100644
--- a/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/token-panel-pro/TokenContent.tsx
+++ b/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/token-panel-pro/TokenContent.tsx
@@ -10,7 +10,7 @@
import { CaretRightOutlined, ExpandOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import type { ThemeConfig } from '@nocobase/client';
import { StablePopover } from '@nocobase/client';
-import { Button, Checkbox, Collapse, ConfigProvider, Switch, Tooltip, Typography } from 'antd';
+import { Button, Checkbox, Collapse, ConfigProvider, InputNumber, Switch, Tooltip, Typography } from 'antd';
import seed from 'antd/es/theme/themes/seed';
import classNames from 'classnames';
import type { FC } from 'react';
@@ -346,6 +346,9 @@ const SeedTokenPreview: FC = ({ theme, tokenName, disabled, alph
/>
)}
{tokenName === 'wireframe' && }
+ {['siderWidth'].includes(tokenName) && (
+
+ )}
);
};
diff --git a/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/token-panel-pro/token-meta.json b/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/token-panel-pro/token-meta.json
index 54ff50a523..5209098ea7 100644
--- a/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/token-panel-pro/token-meta.json
+++ b/packages/plugins/@nocobase/plugin-theme-editor/src/client/antd-token-previewer/token-panel-pro/token-meta.json
@@ -1592,6 +1592,14 @@
"type": "boolean",
"source": "seed"
},
+ "siderWidth": {
+ "name": "侧边栏宽度(px)",
+ "nameEn": "Sider width (px)",
+ "desc": "调整侧边栏展示区域的宽度,宽度单位通常为像素(px)",
+ "descEn": "Adjust the width of the sidebar display area. The width is usually measured in pixels (px).",
+ "type": "number",
+ "source": "map"
+ },
"zIndexBase": {
"name": "基础 zIndex",
"nameEn": "Base zIndex",
diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/package.json b/packages/plugins/@nocobase/plugin-ui-schema-storage/package.json
index ad06a3554e..693128c61c 100644
--- a/packages/plugins/@nocobase/plugin-ui-schema-storage/package.json
+++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "UI schema 存储服务",
"description": "Provides centralized UI schema storage service.",
"description.zh-CN": "提供中心化的 UI schema 存储服务。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/ui-schema-storage",
diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts
index f13461d128..17a3821d72 100644
--- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts
+++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts
@@ -10,7 +10,7 @@
import { Cache } from '@nocobase/cache';
import { Repository, Transaction, Transactionable } from '@nocobase/database';
import { uid } from '@nocobase/utils';
-import lodash from 'lodash';
+import { default as _, default as lodash } from 'lodash';
import { ChildOptions, SchemaNode, TargetPosition } from './dao/ui_schema_node_dao';
export interface GetJsonSchemaOptions {
@@ -297,6 +297,23 @@ export class UiSchemaRepository extends Repository {
);
}
+ async emitAfterSaveEvent(s, options) {
+ if (!s?.schema) {
+ return;
+ }
+ const keys = ['title', 'description', 'x-component-props.title', 'x-decorator-props.title'];
+ let r = false;
+ for (const key of keys) {
+ if (_.get(s?.schema, key)) {
+ r = true;
+ break;
+ }
+ }
+ if (r) {
+ await this.database.emitAsync(`${this.collection.name}.afterSave`, s, options);
+ }
+ }
+
@transaction()
async patch(newSchema: any, options?) {
const { transaction } = options;
@@ -305,8 +322,8 @@ export class UiSchemaRepository extends Repository {
if (!newSchema['properties']) {
const s = await this.model.findByPk(rootUid, { transaction });
s.set('schema', { ...s.toJSON(), ...newSchema });
- // console.log(s.toJSON());
await s.save({ transaction, hooks: false });
+ await this.emitAfterSaveEvent(s, options);
if (newSchema['x-server-hooks']) {
await this.database.emitAsync(`${this.collection.name}.afterSave`, s, options);
}
@@ -488,8 +505,14 @@ export class UiSchemaRepository extends Repository {
}
const result = await this[`insert${lodash.upperFirst(position)}`](target, schema, options);
+
+ const s = await this.model.findByPk(schema, { transaction });
+
+ await this.emitAfterSaveEvent(s, options);
+
// clear target schema path cache
await this.clearXUidPathCache(result['x-uid'], transaction);
+
return result;
}
@@ -869,6 +892,8 @@ WHERE TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and TreeTable.sort
},
);
+ await this.emitAfterSaveEvent(nodeModel, { transaction });
+
if (schema['x-server-hooks']) {
await this.database.emitAsync(`${this.collection.name}.afterSave`, nodeModel, { transaction });
}
diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts
index 1ad9a34e0a..befe842063 100644
--- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts
+++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts
@@ -9,6 +9,8 @@
import { MagicAttributeModel } from '@nocobase/database';
import { Plugin } from '@nocobase/server';
+import PluginLocalizationServer from '@nocobase/plugin-localization';
+import { tval } from '@nocobase/utils';
import { uid } from '@nocobase/utils';
import path, { resolve } from 'path';
import { uiSchemaActions } from './actions/ui-schema-action';
@@ -17,6 +19,19 @@ import UiSchemaRepository from './repository';
import { ServerHooks } from './server-hooks';
import { ServerHookModel } from './server-hooks/model';
+export const compile = (title: string) => (title || '').replace(/{{\s*t\(["|'|`](.*)["|'|`]\)\s*}}/g, '$1');
+
+function extractFields(obj) {
+ return [
+ obj.title,
+ obj.description,
+ obj['x-component-props']?.title,
+ obj['x-component-props']?.description,
+ obj['x-decorator-props']?.title,
+ obj['x-decorator-props']?.description,
+ ].filter((value) => value !== undefined && value !== '');
+}
+
export class PluginUISchemaStorageServer extends Plugin {
serverHooks: ServerHooks;
@@ -28,7 +43,7 @@ export class PluginUISchemaStorageServer extends Plugin {
async beforeLoad() {
const db = this.app.db;
-
+ const pm = this.app.pm;
this.serverHooks = new ServerHooks(db);
this.app.db.registerModels({ MagicAttributeModel, UiSchemaModel, ServerHookModel });
@@ -51,6 +66,19 @@ export class PluginUISchemaStorageServer extends Plugin {
}
});
+ db.on('uiSchemas.afterSave', async function setUid(model, options) {
+ const localizationPlugin = pm.get('localization') as PluginLocalizationServer;
+ const texts = [];
+ const changedFields = extractFields(model.toJSON());
+ if (!changedFields.length) {
+ return;
+ }
+ changedFields.forEach((field) => {
+ field && texts.push({ text: compile(field), module: `resources.ui-schema-storage` });
+ });
+ await localizationPlugin?.addNewTexts?.(texts, options);
+ });
+
db.on('uiSchemas.afterCreate', async function insertSchema(model, options) {
const { transaction } = options;
const uiSchemaRepository = db.getCollection('uiSchemas').repository as UiSchemaRepository;
@@ -125,6 +153,34 @@ export class PluginUISchemaStorageServer extends Plugin {
]);
await this.importCollections(resolve(__dirname, 'collections'));
+ // this.registerLocalizationSource();
+ }
+
+ registerLocalizationSource() {
+ const localizationPlugin = this.app.pm.get('localization') as PluginLocalizationServer;
+ if (!localizationPlugin) {
+ return;
+ }
+ localizationPlugin.sourceManager.registerSource('ui-schema-storage', {
+ title: tval('UiSchema'),
+ sync: async (ctx) => {
+ const uiSchemas = await ctx.db.getRepository('uiSchemas').find({
+ raw: true,
+ });
+ const resources = {};
+ uiSchemas.forEach((route: { schema?: any }) => {
+ const changedFields = extractFields(route.schema);
+ if (changedFields.length) {
+ changedFields.forEach((field) => {
+ resources[field] = '';
+ });
+ }
+ });
+ return {
+ 'ui-schema-storage': resources,
+ };
+ },
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/package.json b/packages/plugins/@nocobase/plugin-user-data-sync/package.json
index 46b76a82f4..a156748c19 100644
--- a/packages/plugins/@nocobase/plugin-user-data-sync/package.json
+++ b/packages/plugins/@nocobase/plugin-user-data-sync/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "用户数据同步",
"description": "Reigster and manage extensible user data synchronization sources, with HTTP API provided by default. Support for synchronizing data to resources such as users and departments.",
"description.zh-CN": "注册和管理可扩展的用户数据同步来源,默认提供 HTTP API。支持向用户和部门等资源同步数据。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"main": "dist/server/index.js",
"peerDependencies": {
"@nocobase/client": "1.x",
diff --git a/packages/plugins/@nocobase/plugin-users/package.json b/packages/plugins/@nocobase/plugin-users/package.json
index bcf2e8d5c2..f4f83dae2e 100644
--- a/packages/plugins/@nocobase/plugin-users/package.json
+++ b/packages/plugins/@nocobase/plugin-users/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "用户",
"description": "Provides basic user model, as well as created by and updated by fields.",
"description.zh-CN": "提供了基础的用户模型,以及创建人和最后更新人字段。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/users",
diff --git a/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts b/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts
index ae2ab602bc..a002aaa929 100644
--- a/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts
+++ b/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts
@@ -12,16 +12,39 @@ import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
import _ from 'lodash';
import { namespace } from '..';
import { ValidationError, ValidationErrorItem } from 'sequelize';
-import PluginSystemSettingsServer from '@nocobase/plugin-system-settings';
+
+function findNonVoidTypeObjects(obj: any, path = '', results = []): { name: string; props: any }[] {
+ for (const key in obj) {
+ const value = obj[key];
+
+ const currentPath = path ? `${path}.${key}` : key;
+
+ if (value && typeof value === 'object') {
+ if (value.type && value.type !== 'void') {
+ results.push({
+ name: key,
+ props: value,
+ });
+ }
+
+ if (value.properties) {
+ findNonVoidTypeObjects(value.properties, `${currentPath}.properties`, results);
+ } else {
+ findNonVoidTypeObjects(value, currentPath, results);
+ }
+ }
+ }
+
+ return results;
+}
function parseProfileFormSchema(schema: any) {
const properties = _.get(schema, 'properties.form.properties.edit.properties.grid.properties') || {};
const fields = [];
const requiredFields = [];
- Object.values(properties).forEach((row: any) => {
- const col = Object.values(row.properties)[0] as any;
- const [name, props] = Object.entries(col.properties)[0];
- if (props['x-read-pretty'] || props['x-disable']) {
+ const configs = findNonVoidTypeObjects(properties);
+ Object.values(configs).forEach(({ name, props }) => {
+ if (props['x-read-pretty'] || props['x-disabled']) {
return;
}
if (props['required']) {
diff --git a/packages/plugins/@nocobase/plugin-verification/package.json b/packages/plugins/@nocobase/plugin-verification/package.json
index f39eca4ad0..ea37012e50 100644
--- a/packages/plugins/@nocobase/plugin-verification/package.json
+++ b/packages/plugins/@nocobase/plugin-verification/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "验证",
"description": "User identity verification management, including SMS, TOTP authenticator, with extensibility.",
"description.zh-CN": "用户身份验证管理,包含短信、TOTP 认证器等,可扩展。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/verification",
diff --git a/packages/plugins/@nocobase/plugin-workflow-action-trigger/package.json b/packages/plugins/@nocobase/plugin-workflow-action-trigger/package.json
index 3be6ba6f36..44f2eaa1e3 100644
--- a/packages/plugins/@nocobase/plugin-workflow-action-trigger/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-action-trigger/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:操作后事件",
"description": "Triggered after the completion of a request initiated through an action button or API, such as after adding, updating, deleting data, or \"submit to workflow\". Suitable for data processing, sending notifications, etc., after actions are completed.",
"description.zh-CN": "通过操作按钮或 API 发起请求并在执行完成后触发,比如新增、更新、删除数据或者“提交至工作流”之后。适用于在操作完成后进行数据处理、发送通知等。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/plugins/workflow-action-trigger",
diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/package.json b/packages/plugins/@nocobase/plugin-workflow-aggregate/package.json
index e05e37f228..05f78f5de1 100644
--- a/packages/plugins/@nocobase/plugin-workflow-aggregate/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:聚合查询节点",
"description": "Used to aggregate data against the database in workflow, such as: statistics, sum, average, etc.",
"description.zh-CN": "可用于在工作流中对数据库进行聚合查询,如:统计数量、求和、平均值等。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-aggregate",
diff --git a/packages/plugins/@nocobase/plugin-workflow-delay/package.json b/packages/plugins/@nocobase/plugin-workflow-delay/package.json
index 6254c65066..31f12b8bf1 100644
--- a/packages/plugins/@nocobase/plugin-workflow-delay/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-delay/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:延时节点",
"description": "Could be used in workflow parallel branch for waiting other branches.",
"description.zh-CN": "可用于工作流并行分支中等待其他分支执行完成。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-delay",
diff --git a/packages/plugins/@nocobase/plugin-workflow-delay/src/client/DelayInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-delay/src/client/DelayInstruction.tsx
index 81935fa347..245f1d6270 100644
--- a/packages/plugins/@nocobase/plugin-workflow-delay/src/client/DelayInstruction.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow-delay/src/client/DelayInstruction.tsx
@@ -8,11 +8,10 @@
*/
import React from 'react';
-import { InputNumber, Select } from 'antd';
+import { Space } from 'antd';
import { HourglassOutlined } from '@ant-design/icons';
-import { css, useCompile } from '@nocobase/client';
-import { Instruction, JOB_STATUS } from '@nocobase/plugin-workflow/client';
+import { Instruction, JOB_STATUS, WorkflowVariableInput } from '@nocobase/plugin-workflow/client';
import { NAMESPACE } from '../locale';
@@ -24,47 +23,6 @@ const UnitOptions = [
{ value: 604800_000, label: `{{t('Weeks', { ns: "workflow" })}}` },
];
-function getNumberOption(v) {
- return UnitOptions.slice()
- .reverse()
- .find((item) => !(v % item.value));
-}
-
-function Duration({ value = 60000, onChange }) {
- const compile = useCompile();
- const option = getNumberOption(value);
- const quantity = Math.round(value / option.value);
-
- return (
-
- onChange(Math.round(v * option.value))}
- className="auto-width"
- />
- onChange(Math.round(quantity * unit))}
- className="auto-width"
- options={UnitOptions.map((item) => ({
- value: item.value,
- label: compile(item.label),
- }))}
- />
-
- );
-}
-
export default class extends Instruction {
title = `{{t("Delay", { ns: "${NAMESPACE}" })}}`;
type = 'delay';
@@ -73,12 +31,40 @@ export default class extends Instruction {
icon = ( );
fieldset = {
duration: {
- type: 'number',
+ type: 'void',
title: `{{t("Duration", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
- 'x-component': 'Duration',
- default: 60000,
+ 'x-component': 'Space.Compact',
required: true,
+ properties: {
+ unit: {
+ type: 'number',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Select',
+ 'x-component-props': {
+ placeholder: `{{t("Unit", { ns: "${NAMESPACE}" })}}`,
+ className: 'auto-width',
+ allowClear: false,
+ },
+ enum: UnitOptions,
+ default: 60_000,
+ },
+ duration: {
+ type: 'number',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'WorkflowVariableInput',
+ 'x-component-props': {
+ placeholder: `{{t("Duration", { ns: "${NAMESPACE}" })}}`,
+ useTypedConstant: [['number', { min: 1 }]],
+ nullable: false,
+ parseOptions: {
+ defaultTypeOnNull: 'number',
+ },
+ },
+ default: 1,
+ required: true,
+ },
+ },
},
endStatus: {
type: 'number',
@@ -94,7 +80,8 @@ export default class extends Instruction {
},
};
components = {
- Duration,
+ WorkflowVariableInput,
+ Space,
};
isAvailable({ engine, workflow, upstream, branchIndex }) {
return !engine.isWorkflowSync(workflow);
diff --git a/packages/plugins/@nocobase/plugin-workflow-delay/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-workflow-delay/src/locale/zh-CN.json
index 0eb731514f..bee971650b 100644
--- a/packages/plugins/@nocobase/plugin-workflow-delay/src/locale/zh-CN.json
+++ b/packages/plugins/@nocobase/plugin-workflow-delay/src/locale/zh-CN.json
@@ -2,6 +2,7 @@
"Delay": "延时",
"Delay a period of time and then continue or exit the process. Can be used to set wait or timeout times in parallel branches.": "延时一段时间,然后继续或退出流程。可以用于并行分支中等待其他分支或设置超时时间。",
"Duration": "时长",
+ "Unit": "单位",
"End status": "到时状态",
"Select status": "选择状态",
"Succeed and continue": "通过并继续",
diff --git a/packages/plugins/@nocobase/plugin-workflow-delay/src/server/DelayInstruction.ts b/packages/plugins/@nocobase/plugin-workflow-delay/src/server/DelayInstruction.ts
index a4d476a6a2..31d5c053d5 100644
--- a/packages/plugins/@nocobase/plugin-workflow-delay/src/server/DelayInstruction.ts
+++ b/packages/plugins/@nocobase/plugin-workflow-delay/src/server/DelayInstruction.ts
@@ -74,7 +74,7 @@ export default class extends Instruction {
schedule(job) {
const now = new Date();
const createdAt = Date.parse(job.createdAt);
- const delay = createdAt + job.node.config.duration - now.getTime();
+ const delay = createdAt + job.result - now.getTime();
if (delay > 0) {
const trigger = this.trigger.bind(this, job);
this.timers.set(job.id, setTimeout(trigger, delay));
@@ -96,9 +96,10 @@ export default class extends Instruction {
}
async run(node, prevJob, processor: Processor) {
+ const duration = processor.getParsedValue(node.config.duration || 1, node.id) * (node.config.unit || 1_000);
const job = await processor.saveJob({
status: JOB_STATUS.PENDING,
- result: null,
+ result: duration,
nodeId: node.id,
nodeKey: node.key,
upstreamId: prevJob?.id ?? null,
@@ -108,7 +109,7 @@ export default class extends Instruction {
// add to schedule
this.schedule(job);
- return processor.exit();
+ return null;
}
async resume(node, prevJob, processor: Processor) {
diff --git a/packages/plugins/@nocobase/plugin-workflow-delay/src/server/__tests__/instruction.test.ts b/packages/plugins/@nocobase/plugin-workflow-delay/src/server/__tests__/instruction.test.ts
index 464d572cab..1335c06997 100644
--- a/packages/plugins/@nocobase/plugin-workflow-delay/src/server/__tests__/instruction.test.ts
+++ b/packages/plugins/@nocobase/plugin-workflow-delay/src/server/__tests__/instruction.test.ts
@@ -47,7 +47,8 @@ describe('workflow > instructions > delay', () => {
const n1 = await workflow.createNode({
type: 'delay',
config: {
- duration: 2000,
+ duration: 2,
+ unit: 1000,
endStatus: JOB_STATUS.RESOLVED,
},
});
@@ -73,7 +74,8 @@ describe('workflow > instructions > delay', () => {
const n1 = await workflow.createNode({
type: 'delay',
config: {
- duration: 2000,
+ duration: 2,
+ unit: 1000,
endStatus: JOB_STATUS.FAILED,
},
});
@@ -95,11 +97,49 @@ describe('workflow > instructions > delay', () => {
expect(j2.status).toBe(JOB_STATUS.FAILED);
});
+ it('duration by variable', async () => {
+ const n1 = await workflow.createNode({
+ type: 'echoVariable',
+ config: {
+ variable: 2,
+ },
+ });
+
+ const n2 = await workflow.createNode({
+ type: 'delay',
+ config: {
+ duration: `{{$jobsMapByNodeKey.${n1.key}}}`,
+ unit: 1000,
+ endStatus: JOB_STATUS.RESOLVED,
+ },
+ upstreamId: n1.id,
+ });
+
+ await n1.setDownstream(n2);
+
+ const post = await PostRepo.create({ values: { title: 't1' } });
+
+ await sleep(500);
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toEqual(EXECUTION_STATUS.STARTED);
+ const [, j1] = await e1.getJobs({ order: [['id', 'ASC']] });
+ expect(j1.status).toBe(JOB_STATUS.PENDING);
+
+ await sleep(2000);
+
+ const [e2] = await workflow.getExecutions();
+ expect(e2.status).toEqual(EXECUTION_STATUS.RESOLVED);
+ const [, j2] = await e2.getJobs({ order: [['id', 'ASC']] });
+ expect(j2.status).toBe(JOB_STATUS.RESOLVED);
+ });
+
it('delay to resolve and downstream node error', async () => {
const n1 = await workflow.createNode({
type: 'delay',
config: {
- duration: 2000,
+ duration: 2,
+ unit: 1000,
endStatus: JOB_STATUS.RESOLVED,
},
});
@@ -139,7 +179,8 @@ describe('workflow > instructions > delay', () => {
await workflow.createNode({
type: 'delay',
config: {
- duration: 2000,
+ duration: 2,
+ unit: 1000,
endStatus: JOB_STATUS.RESOLVED,
},
});
diff --git a/packages/plugins/@nocobase/plugin-workflow-delay/src/server/__tests__/migrations/20250403223032-add-duration-unit.test.ts b/packages/plugins/@nocobase/plugin-workflow-delay/src/server/__tests__/migrations/20250403223032-add-duration-unit.test.ts
new file mode 100644
index 0000000000..addafa71a1
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-delay/src/server/__tests__/migrations/20250403223032-add-duration-unit.test.ts
@@ -0,0 +1,86 @@
+/**
+ * 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 { createMockServer } from '@nocobase/test';
+import Migration from '../../migrations/20250403223032-add-duration-unit';
+
+describe('20250403223032-add-duration-unit', () => {
+ let app;
+ let migration;
+ let NodeRepo;
+
+ beforeEach(async () => {
+ app = await createMockServer({
+ plugins: ['nocobase'],
+ });
+ await app.version.update('1.6.0');
+
+ migration = new Migration({ db: app.db, app } as any);
+
+ NodeRepo = app.db.getRepository('flow_nodes');
+ });
+
+ afterEach(async () => {
+ await app.destroy();
+ });
+
+ it(`duration as null`, async () => {
+ const n1 = await NodeRepo.create({
+ values: {
+ type: 'delay',
+ config: {},
+ },
+ });
+
+ await migration.up();
+
+ const n2 = await NodeRepo.findOne({
+ filterByTk: n1.id,
+ });
+ expect(n2.config.duration).toBeFalsy();
+ });
+
+ it(`duration as number (second)`, async () => {
+ const n1 = await NodeRepo.create({
+ values: {
+ type: 'delay',
+ config: {
+ duration: 1000,
+ },
+ },
+ });
+
+ await migration.up();
+
+ const n2 = await NodeRepo.findOne({
+ filterByTk: n1.id,
+ });
+ expect(n2.config.duration).toBe(1);
+ expect(n2.config.unit).toBe(1000);
+ });
+
+ it(`duration as number (day)`, async () => {
+ const n1 = await NodeRepo.create({
+ values: {
+ type: 'delay',
+ config: {
+ duration: 1000 * 60 * 60 * 24 * 2,
+ },
+ },
+ });
+
+ await migration.up();
+
+ const n2 = await NodeRepo.findOne({
+ filterByTk: n1.id,
+ });
+ expect(n2.config.duration).toBe(2);
+ expect(n2.config.unit).toBe(1000 * 60 * 60 * 24);
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-workflow-delay/src/server/migrations/20250403223032-add-duration-unit.ts b/packages/plugins/@nocobase/plugin-workflow-delay/src/server/migrations/20250403223032-add-duration-unit.ts
new file mode 100644
index 0000000000..9c5600caa3
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-delay/src/server/migrations/20250403223032-add-duration-unit.ts
@@ -0,0 +1,54 @@
+/**
+ * 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 { Migration } from '@nocobase/server';
+
+const UnitOptions = [604800_000, 86400_000, 3600_000, 60_000, 1_000];
+
+function getNumberOption(v) {
+ return UnitOptions.find((item) => !(v % item));
+}
+
+export default class extends Migration {
+ appVersion = '<1.7.0';
+ async up() {
+ const { db } = this.context;
+
+ const NodeRepo = db.getRepository('flow_nodes');
+ await db.sequelize.transaction(async (transaction) => {
+ const nodes = await NodeRepo.find({
+ filter: {
+ type: 'delay',
+ },
+ transaction,
+ });
+
+ await nodes.reduce(
+ (promise, node) =>
+ promise.then(async () => {
+ if (node.config.unit) {
+ return;
+ }
+ if (!node.config.duration) {
+ return;
+ }
+ const unit = getNumberOption(node.config.duration);
+ const duration = node.config.duration / unit;
+ node.set('config', { ...node.config, duration, unit });
+ node.changed('config', true);
+ await node.save({
+ silent: true,
+ transaction,
+ });
+ }),
+ Promise.resolve(),
+ );
+ });
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json
index 50224e46b4..2c5f15c9bd 100644
--- a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:动态表达式计算节点",
"description": "Useful plugin for doing dynamic calculation based on expression collection records in workflow.",
"description.zh-CN": "用于在工作流中进行基于数据行的动态表达式计算。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-dynamic-calculation",
diff --git a/packages/plugins/@nocobase/plugin-workflow-loop/package.json b/packages/plugins/@nocobase/plugin-workflow-loop/package.json
index 0ab3367633..cb001a0e24 100644
--- a/packages/plugins/@nocobase/plugin-workflow-loop/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-loop/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:循环节点",
"description": "Used to repeat the sub-process processing of each value in an array, and can also be used for fixed times of sub-process processing.",
"description.zh-CN": "用于对一个数组中的每个值进行重复的子流程处理,也可用于固定次数的重复子流程处理。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-loop",
diff --git a/packages/plugins/@nocobase/plugin-workflow-mailer/package.json b/packages/plugins/@nocobase/plugin-workflow-mailer/package.json
index 9eefa01b2a..8ea55cc31f 100644
--- a/packages/plugins/@nocobase/plugin-workflow-mailer/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-mailer/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:邮件发送节点",
"description": "Send email in workflow.",
"description.zh-CN": "可用于在工作流中发送电子邮件。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-smtp-mailer",
diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/package.json b/packages/plugins/@nocobase/plugin-workflow-manual/package.json
index d87db76e0f..c566a6a25a 100644
--- a/packages/plugins/@nocobase/plugin-workflow-manual/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-manual/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:人工处理节点",
"description": "Could be used for workflows which some of decisions are made by users.",
"description.zh-CN": "用于人工控制部分决策的流程。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-manual",
diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx
index beeae222c4..5a90d0daad 100644
--- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx
@@ -8,6 +8,7 @@
*/
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
import { useField, useFieldSchema, useForm } from '@formily/react';
import { FormLayout } from '@formily/antd-v5';
import { Button, Card, ConfigProvider, Descriptions, Space, Spin, Tag } from 'antd';
@@ -33,9 +34,12 @@ import {
useActionContext,
useCurrentUserContext,
useFormBlockContext,
- useTableBlockContext,
+ useListBlockContext,
List,
OpenModeProvider,
+ ActionContextProvider,
+ useRequest,
+ CollectionRecordProvider,
} from '@nocobase/client';
import WorkflowPlugin, {
DetailsBlockProvider,
@@ -44,14 +48,14 @@ import WorkflowPlugin, {
useAvailableUpstreams,
useFlowContext,
EXECUTION_STATUS,
- JOB_STATUS,
WorkflowTitle,
+ usePopupRecordContext,
} from '@nocobase/plugin-workflow/client';
import { NAMESPACE, useLang } from '../locale';
import { FormBlockProvider } from './instruction/FormBlockProvider';
import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig';
-import { TaskStatusOptionsMap } from '../common/constants';
+import { TaskStatusOptionsMap, TASK_STATUS } from '../common/constants';
function TaskStatusColumn(props) {
const recordData = useCollectionRecordData();
@@ -291,11 +295,12 @@ function useSubmit() {
const { values, submit } = useForm();
const field = useField();
const buttonSchema = useFieldSchema();
- const { service } = useTableBlockContext();
+ const { service } = useListBlockContext();
const { userJob, execution } = useFlowContext();
const { name: actionKey } = buttonSchema;
const { name: formKey } = buttonSchema.parent.parent;
const { assignedValues = {} } = buttonSchema?.['x-action-settings'] ?? {};
+
return {
async run() {
if (execution.status || userJob.status) {
@@ -611,67 +616,47 @@ function ContentDetailWithTitle(props) {
function TaskItem() {
const token = useAntdToken();
- const [visible, setVisible] = useState(false);
const record = useCollectionRecordData();
- const { t } = useTranslation();
- // const { defaultOpenMode } = useOpenModeContext();
- // const { openPopup } = usePopupUtils();
- // const { isPopupVisibleControlledByURL } = usePopupSettings();
- const onOpen = useCallback((e: React.MouseEvent) => {
- const targetElement = e.target as Element; // 将事件目标转换为Element类型
- const currentTargetElement = e.currentTarget as Element;
- if (currentTargetElement.contains(targetElement)) {
- setVisible(true);
- // if (!isPopupVisibleControlledByURL()) {
- // } else {
- // openPopup({
- // // popupUidUsedInURL: 'job',
- // customActionSchema: {
- // type: 'void',
- // 'x-uid': 'job-view',
- // 'x-action-context': {
- // dataSource: 'main',
- // collection: 'workflowManualTasks',
- // doNotUpdateContext: true,
- // },
- // properties: {},
- // },
- // });
- // }
- }
- e.stopPropagation();
- }, []);
+ const navigate = useNavigate();
+ const { setRecord } = usePopupRecordContext();
+ const onOpen = useCallback(
+ (e: React.MouseEvent) => {
+ const targetElement = e.target as Element; // 将事件目标转换为Element类型
+ const currentTargetElement = e.currentTarget as Element;
+ if (currentTargetElement.contains(targetElement)) {
+ setRecord(record);
+ navigate(`./${record.id}`);
+ }
+ e.stopPropagation();
+ },
+ [navigate, record.id],
+ );
return (
- <>
- }
- className={css`
- .ant-card-extra {
- color: ${token.colorTextDescription};
- }
- `}
- >
-
-
-
-
-
- >
+ }
+ className={css`
+ .ant-card-extra {
+ color: ${token.colorTextDescription};
+ }
+ `}
+ >
+
+
);
}
const StatusFilterMap = {
pending: {
- status: JOB_STATUS.PENDING,
+ status: TASK_STATUS.PENDING,
'execution.status': EXECUTION_STATUS.STARTED,
},
completed: {
- status: JOB_STATUS.RESOLVED,
+ status: [TASK_STATUS.RESOLVED, TASK_STATUS.REJECTED],
},
};
@@ -734,7 +719,9 @@ function TodoExtraActions() {
export const manualTodo = {
title: `{{t("My manual tasks", { ns: "${NAMESPACE}" })}}`,
collection: 'workflowManualTasks',
+ action: 'listMine',
useActionParams: useTodoActionParams,
- component: TaskItem,
- extraActions: TodoExtraActions,
+ Actions: TodoExtraActions,
+ Item: TaskItem,
+ Detail: Drawer,
};
diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/common/constants.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/common/constants.ts
index dd4acc5d20..a59b843700 100644
--- a/packages/plugins/@nocobase/plugin-workflow-manual/src/common/constants.ts
+++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/common/constants.ts
@@ -14,7 +14,7 @@ export const MANUAL_TASK_TYPE = 'manual';
export const TASK_STATUS = {
PENDING: 0,
RESOLVED: 1,
- REJECTED: -1,
+ REJECTED: -5,
};
export const TaskStatusOptions = [
diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/Plugin.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/Plugin.ts
index 7338d0958e..08e931293b 100644
--- a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/Plugin.ts
+++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/Plugin.ts
@@ -16,13 +16,14 @@ import * as jobActions from './actions';
import ManualInstruction from './ManualInstruction';
import { MANUAL_TASK_TYPE } from '../common/constants';
+import { Model } from '@nocobase/database';
-interface WorkflowManualTaskModel {
- id: number;
- userId: number;
- workflowId: number;
- executionId: number;
- status: number;
+class WorkflowManualTaskModel extends Model {
+ declare id: number;
+ declare userId: number;
+ declare workflowId: number;
+ declare executionId: number;
+ declare status: number;
}
export default class extends Plugin {
@@ -50,12 +51,18 @@ export default class extends Plugin {
},
});
- this.app.acl.allow('workflowManualTasks', ['list', 'get', 'submit'], 'loggedIn');
+ this.app.acl.allow('workflowManualTasks', ['list', 'listMine', 'get', 'submit'], 'loggedIn');
const workflowPlugin = this.app.pm.get(WorkflowPlugin) as WorkflowPlugin;
workflowPlugin.registerInstruction('manual', ManualInstruction);
- this.db.on('workflowManualTasks.afterSave', async (task: WorkflowManualTaskModel, options) => {
+ this.db.on('workflowManualTasks.afterSave', async (task: WorkflowManualTaskModel, { transaction }) => {
+ // const allCount = await (task.constructor as typeof WorkflowManualTaskModel).count({
+ // where: {
+ // userId: task.userId,
+ // },
+ // transaction,
+ // });
await workflowPlugin.toggleTaskStatus(
{
type: MANUAL_TASK_TYPE,
@@ -63,8 +70,9 @@ export default class extends Plugin {
userId: task.userId,
workflowId: task.workflowId,
},
- Boolean(task.status),
- options,
+ task.status === JOB_STATUS.PENDING,
+ // allCount,
+ { transaction },
);
});
}
diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/tasks.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/tasks.test.ts
new file mode 100644
index 0000000000..c866136331
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/tasks.test.ts
@@ -0,0 +1,150 @@
+/**
+ * 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 Database from '@nocobase/database';
+import { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow';
+import { getApp, sleep } from '@nocobase/plugin-workflow-test';
+import { MockServer } from '@nocobase/test';
+import { TASK_STATUS } from '../../common/constants';
+
+// NOTE: skipped because time is not stable on github ci, but should work in local
+describe('workflow > instructions > manual > task center', () => {
+ let app: MockServer;
+ let agent;
+ let userAgents;
+ let db: Database;
+ let PostRepo;
+ let WorkflowModel;
+ let workflow;
+ let UserModel;
+ let users;
+ let UserJobModel;
+
+ beforeEach(async () => {
+ app = await getApp({
+ plugins: ['users', 'auth', 'workflow-manual'],
+ });
+ // await app.getPlugin('auth').install();
+ agent = app.agent();
+ db = app.db;
+ WorkflowModel = db.getCollection('workflows').model;
+ PostRepo = db.getCollection('posts').repository;
+ UserModel = db.getCollection('users').model;
+ UserJobModel = db.getModel('workflowManualTasks');
+
+ users = await UserModel.bulkCreate([
+ { id: 2, nickname: 'a' },
+ { id: 3, nickname: 'b' },
+ ]);
+
+ userAgents = await Promise.all(users.map((user) => app.agent().login(user)));
+
+ workflow = await WorkflowModel.create({
+ enabled: true,
+ type: 'collection',
+ config: {
+ mode: 1,
+ collection: 'posts',
+ },
+ });
+ });
+
+ afterEach(() => app.destroy());
+
+ describe('listMine', () => {
+ it('member', async () => {
+ const n1 = await workflow.createNode({
+ type: 'manual',
+ config: {
+ assignees: [users[0].id],
+ forms: {
+ f1: {
+ type: 'update',
+ actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
+ collection: 'posts',
+ filter: {
+ id: '{{$context.data.id}}',
+ },
+ },
+ },
+ },
+ });
+
+ const post = await PostRepo.create({ values: { title: 't1' } });
+
+ await sleep(500);
+
+ const UserJobModel = db.getModel('workflowManualTasks');
+ const pendingJobs = await UserJobModel.findAll({
+ order: [['userId', 'ASC']],
+ });
+ expect(pendingJobs.length).toBe(1);
+
+ const res1 = await userAgents[0].resource('workflowManualTasks').listMine();
+ expect(res1.status).toBe(200);
+ expect(res1.body.data.length).toBe(1);
+
+ const res2 = await userAgents[0].resource('workflowManualTasks').listMine({
+ filter: {
+ status: TASK_STATUS.PENDING,
+ },
+ });
+ expect(res2.status).toBe(200);
+ expect(res2.body.data.length).toBe(1);
+
+ const res3 = await userAgents[0].resource('workflowManualTasks').listMine({
+ filter: {
+ status: TASK_STATUS.RESOLVED,
+ },
+ });
+ expect(res3.status).toBe(200);
+ expect(res3.body.data.length).toBe(0);
+
+ const res4 = await userAgents[0].resource('workflowManualTasks').submit({
+ filterByTk: pendingJobs[0].get('id'),
+ values: {
+ result: { f1: { title: 't2' }, _: 'resolve' },
+ },
+ });
+ expect(res4.status).toBe(202);
+
+ await sleep(500);
+
+ const [e1] = await workflow.getExecutions({ order: [['createdAt', 'ASC']] });
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const [j1] = await e1.getJobs();
+ expect(j1.status).toBe(JOB_STATUS.RESOLVED);
+ expect(j1.result).toMatchObject({ f1: { title: 't2' } });
+
+ const posts = await PostRepo.find({ order: [['createdAt', 'ASC']] });
+ expect(posts.length).toBe(1);
+ expect(posts[0]).toMatchObject({ title: 't2' });
+
+ const res5 = await userAgents[0].resource('workflowManualTasks').listMine({
+ filter: {
+ status: TASK_STATUS.PENDING,
+ },
+ });
+ expect(res5.status).toBe(200);
+ expect(res5.body.data.length).toBe(0);
+
+ const res6 = await userAgents[0].resource('workflowManualTasks').listMine({
+ filter: {
+ status: TASK_STATUS.RESOLVED,
+ },
+ });
+ expect(res6.status).toBe(200);
+ expect(res6.body.data.length).toBe(1);
+
+ const res7 = await userAgents[0].resource('workflowManualTasks').listMine();
+ expect(res7.status).toBe(200);
+ expect(res7.body.data.length).toBe(1);
+ });
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/actions.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/actions.ts
index 33dccdbeb5..a9ac4c9768 100644
--- a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/actions.ts
+++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/actions.ts
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { Context, utils } from '@nocobase/actions';
+import actions, { Context, utils } from '@nocobase/actions';
import WorkflowPlugin, { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow';
import ManualInstruction from './ManualInstruction';
@@ -111,3 +111,24 @@ export async function submit(context: Context, next) {
plugin.resume(task.job);
}
+
+export async function listMine(context, next) {
+ context.action.mergeParams({
+ filter: {
+ userId: context.state.currentUser.id,
+ $or: [
+ {
+ 'workflow.enabled': true,
+ },
+ {
+ 'workflow.enabled': false,
+ status: {
+ $ne: JOB_STATUS.PENDING,
+ },
+ },
+ ],
+ },
+ });
+
+ return actions.list(context, next);
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow-notification/package.json b/packages/plugins/@nocobase/plugin-workflow-notification/package.json
index 9683df1bc2..d99263dafb 100644
--- a/packages/plugins/@nocobase/plugin-workflow-notification/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-notification/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:通知节点",
"description": "Send notification in workflow.",
"description.zh-CN": "可用于在工作流中发送各类通知。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-smtp-mailer",
diff --git a/packages/plugins/@nocobase/plugin-workflow-parallel/package.json b/packages/plugins/@nocobase/plugin-workflow-parallel/package.json
index bd986a36c2..ca93769c56 100644
--- a/packages/plugins/@nocobase/plugin-workflow-parallel/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-parallel/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:并行分支节点",
"description": "Could be used for parallel execution of branch processes in the workflow.",
"description.zh-CN": "用于在工作流中需要并行执行的分支流程。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-parallel",
diff --git a/packages/plugins/@nocobase/plugin-workflow-request/package.json b/packages/plugins/@nocobase/plugin-workflow-request/package.json
index c4877a1798..1e04339826 100644
--- a/packages/plugins/@nocobase/plugin-workflow-request/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-request/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:HTTP 请求节点",
"description": "Send HTTP requests to any HTTP service for data interaction in workflow.",
"description.zh-CN": "可用于在工作流中向任意 HTTP 服务发送请求,进行数据交互。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-request",
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/.npmignore b/packages/plugins/@nocobase/plugin-workflow-response-message/.npmignore
new file mode 100644
index 0000000000..65f5e8779f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/.npmignore
@@ -0,0 +1,2 @@
+/node_modules
+/src
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/README.md b/packages/plugins/@nocobase/plugin-workflow-response-message/README.md
new file mode 100644
index 0000000000..8ced21e948
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/README.md
@@ -0,0 +1 @@
+# @nocobase/plugin-workflow-response-message
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/client.d.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/client.d.ts
new file mode 100644
index 0000000000..6c459cbac4
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/client.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/client';
+export { default } from './dist/client';
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/client.js b/packages/plugins/@nocobase/plugin-workflow-response-message/client.js
new file mode 100644
index 0000000000..b6e3be70e6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/client.js
@@ -0,0 +1 @@
+module.exports = require('./dist/client/index.js');
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/package.json b/packages/plugins/@nocobase/plugin-workflow-response-message/package.json
new file mode 100644
index 0000000000..cc9961a5f0
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@nocobase/plugin-workflow-response-message",
+ "version": "1.7.0-alpha.11",
+ "displayName": "Workflow: Response message",
+ "displayName.zh-CN": "工作流:响应消息",
+ "description": "Used for assemble response message and showing to client in form event and request interception workflows.",
+ "description.zh-CN": "用于在表单事件和请求拦截工作流中组装并向客户端显示响应消息。",
+ "main": "dist/server/index.js",
+ "homepage": "https://docs.nocobase.com/handbook/workflow-response-message",
+ "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/workflow-response-message",
+ "peerDependencies": {
+ "@nocobase/client": "1.x",
+ "@nocobase/plugin-workflow": "1.x",
+ "@nocobase/server": "1.x",
+ "@nocobase/test": "1.x",
+ "@nocobase/utils": "1.x"
+ },
+ "keywords": [
+ "Workflow"
+ ],
+ "gitHead": "080fc78c1a744d47e010b3bbe5840446775800e4"
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/server.d.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/server.d.ts
new file mode 100644
index 0000000000..c41081ddc6
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/server.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/server';
+export { default } from './dist/server';
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/server.js b/packages/plugins/@nocobase/plugin-workflow-response-message/server.js
new file mode 100644
index 0000000000..972842039a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/server.js
@@ -0,0 +1 @@
+module.exports = require('./dist/server/index.js');
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/ResponseMessageInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/ResponseMessageInstruction.tsx
new file mode 100644
index 0000000000..efdd6a7241
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/ResponseMessageInstruction.tsx
@@ -0,0 +1,87 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import React from 'react';
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { Alert, Space } from 'antd';
+
+import {
+ Instruction,
+ RadioWithTooltip,
+ WorkflowVariableInput,
+ WorkflowVariableTextArea,
+} from '@nocobase/plugin-workflow/client';
+
+import { NAMESPACE } from '../locale';
+
+export default class extends Instruction {
+ title = `{{t("Response message", { ns: "${NAMESPACE}" })}}`;
+ type = 'response-message';
+ group = 'extended';
+ description = `{{t("Add response message, will be send to client when process of request ends.", { ns: "${NAMESPACE}" })}}`;
+ icon = ( );
+ fieldset = {
+ message: {
+ type: 'string',
+ title: `{{t("Message content", { ns: "${NAMESPACE}" })}}`,
+ description: `{{t('Supports variables in template.', { ns: "${NAMESPACE}", name: '{{name}}' })}}`,
+ 'x-decorator': 'FormItem',
+ 'x-component': 'WorkflowVariableTextArea',
+ },
+ info: {
+ type: 'void',
+ 'x-component': 'Space',
+ 'x-component-props': {
+ direction: 'vertical',
+ },
+ properties: {
+ success: {
+ type: 'void',
+ 'x-component': 'Alert',
+ 'x-component-props': {
+ type: 'success',
+ showIcon: true,
+ description: `{{t('If the workflow ends normally, the response message will return a success status by default.', { ns: "${NAMESPACE}" })}}`,
+ },
+ },
+ failure: {
+ type: 'void',
+ 'x-component': 'Alert',
+ 'x-component-props': {
+ type: 'error',
+ showIcon: true,
+ description: `{{t('If you want to return a failure status, please add an "End Process" node downstream to terminate the workflow.', { ns: "${NAMESPACE}" })}}`,
+ },
+ },
+ },
+ },
+ };
+ scope = {};
+ components = {
+ RadioWithTooltip,
+ WorkflowVariableTextArea,
+ WorkflowVariableInput,
+ Alert,
+ Space,
+ };
+ isAvailable({ workflow, upstream, branchIndex }) {
+ return (
+ workflow.type === 'request-interception' || (['action', 'custom-action'].includes(workflow.type) && workflow.sync)
+ );
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/index.tsx b/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/index.tsx
new file mode 100644
index 0000000000..258f6c8d8b
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/client/index.tsx
@@ -0,0 +1,31 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { Plugin } from '@nocobase/client';
+import WorkflowPlugin from '@nocobase/plugin-workflow/client';
+
+import ResponseMessageInstruction from './ResponseMessageInstruction';
+
+export class PluginWorkflowResponseMessageClient extends Plugin {
+ async load() {
+ const workflowPlugin = this.app.pm.get('workflow') as WorkflowPlugin;
+ workflowPlugin.registerInstruction('response-message', ResponseMessageInstruction);
+ }
+}
+
+export default PluginWorkflowResponseMessageClient;
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/index.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/index.ts
new file mode 100644
index 0000000000..7d69462f4f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/index.ts
@@ -0,0 +1,20 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+export * from './server';
+export { default } from './server';
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/en-US.json
new file mode 100644
index 0000000000..11832d6610
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/en-US.json
@@ -0,0 +1,6 @@
+{
+ "Response message": "Response message",
+ "Add response message, will be send to client when process of request ends.": "Add response message, will be send to client when process of request ends.",
+ "Message content": "Message content",
+ "Supports variables in template.": "Supports variables in template."
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/index.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/index.ts
new file mode 100644
index 0000000000..543b392f21
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/index.ts
@@ -0,0 +1,25 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { i18n } from '@nocobase/client';
+
+export const NAMESPACE = '@nocobase/plugin-workflow-response-message';
+
+export function lang(key: string, options = {}) {
+ return i18n.t(key, { ...options, ns: NAMESPACE });
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/zh-CN.json
new file mode 100644
index 0000000000..c8b986cace
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/locale/zh-CN.json
@@ -0,0 +1,8 @@
+{
+ "Response message": "响应消息",
+ "Add response message, will be send to client when process of request ends.": "添加响应消息,将在请求处理结束时发送给客户端。",
+ "Message content": "消息内容",
+ "Supports variables in template.": "支持模板变量。",
+ "If the workflow ends normally, the response message will return a success status by default.": "如果工作流正常结束,响应消息默认返回成功状态。",
+ "If you want to return a failure status, please add an \"End Process\" node downstream to terminate the workflow.": "如果希望返回失败状态,请在下游添加“结束流程”节点终止工作流。"
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/Plugin.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/Plugin.ts
new file mode 100644
index 0000000000..cdd6eece72
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/Plugin.ts
@@ -0,0 +1,31 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { Plugin } from '@nocobase/server';
+import PluginWorkflowServer from '@nocobase/plugin-workflow';
+
+import ResponseMessageInstruction from './ResponseMessageInstruction';
+
+export class PluginWorkflowResponseMessageServer extends Plugin {
+ async load() {
+ const workflowPlugin = this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer;
+ workflowPlugin.registerInstruction('response-message', ResponseMessageInstruction);
+ }
+}
+
+export default PluginWorkflowResponseMessageServer;
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/ResponseMessageInstruction.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/ResponseMessageInstruction.ts
new file mode 100644
index 0000000000..e277a44e35
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/ResponseMessageInstruction.ts
@@ -0,0 +1,55 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import { Instruction, Processor, JOB_STATUS, FlowNodeModel } from '@nocobase/plugin-workflow';
+
+interface Config {
+ message?: string;
+}
+
+export default class extends Instruction {
+ async run(node: FlowNodeModel, prevJob, processor: Processor) {
+ const { httpContext } = processor.options;
+
+ if (!httpContext) {
+ return {
+ status: JOB_STATUS.RESOLVED,
+ result: null,
+ };
+ }
+
+ if (!httpContext.state) {
+ httpContext.state = {};
+ }
+
+ if (!httpContext.state.messages) {
+ httpContext.state.messages = [];
+ }
+
+ const message = processor.getParsedValue(node.config.message, node.id);
+
+ if (message) {
+ httpContext.state.messages.push({ message });
+ }
+
+ return {
+ status: JOB_STATUS.RESOLVED,
+ result: message,
+ };
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/instruction.test.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/instruction.test.ts
new file mode 100644
index 0000000000..500a70cefe
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/instruction.test.ts
@@ -0,0 +1,346 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import Database from '@nocobase/database';
+import { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow';
+import { getApp } from '@nocobase/plugin-workflow-test';
+import { MockServer } from '@nocobase/test';
+
+import Plugin from '..';
+
+describe.skip('workflow > instructions > response-message', () => {
+ let app: MockServer;
+ let db: Database;
+ let PostRepo;
+ let WorkflowModel;
+ let workflow;
+ let users;
+ let userAgents;
+
+ beforeEach(async () => {
+ app = await getApp({
+ plugins: ['users', 'auth', 'error-handler', 'workflow-request-interceptor', Plugin],
+ });
+
+ db = app.db;
+
+ PostRepo = db.getCollection('posts').repository;
+
+ WorkflowModel = db.getModel('workflows');
+ workflow = await WorkflowModel.create({
+ enabled: true,
+ type: 'request-interception',
+ config: {
+ global: true,
+ actions: ['create'],
+ collection: 'posts',
+ },
+ });
+
+ const UserModel = db.getCollection('users').model;
+ users = await UserModel.bulkCreate([
+ { id: 2, nickname: 'a' },
+ { id: 3, nickname: 'b' },
+ ]);
+
+ userAgents = await Promise.all(users.map((user) => app.agent().login(user)));
+ });
+
+ afterEach(() => app.destroy());
+
+ describe('no end, pass flow', () => {
+ it('no message', async () => {
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body).toMatchObject({ data: { title: 't1' } });
+ expect(res1.body.messages).toBeUndefined();
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeDefined();
+ expect(post.title).toBe('t1');
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(0);
+ });
+
+ it('has node, but null message', async () => {
+ const n1 = await workflow.createNode({
+ type: 'response-message',
+ });
+
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body).toMatchObject({ data: { title: 't1' } });
+ expect(res1.body.messages).toBeUndefined();
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeDefined();
+ expect(post.title).toBe('t1');
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(1);
+ });
+
+ it('has node, but empty message', async () => {
+ const n1 = await workflow.createNode({
+ type: 'response-message',
+ config: {
+ message: '',
+ },
+ });
+
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body).toMatchObject({ data: { title: 't1' } });
+ expect(res1.body.messages).toBeUndefined();
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeDefined();
+ expect(post.title).toBe('t1');
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(1);
+ });
+
+ it('single static message', async () => {
+ const n1 = await workflow.createNode({
+ type: 'response-message',
+ config: {
+ message: 'm1',
+ },
+ });
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body).toMatchObject({ data: { title: 't1' } });
+ expect(res1.body.messages).toEqual([{ message: 'm1' }]);
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeDefined();
+ expect(post.title).toBe('t1');
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(1);
+ });
+
+ it('multiple static messages', async () => {
+ const n1 = await workflow.createNode({
+ type: 'response-message',
+ config: {
+ message: 'm1',
+ },
+ });
+ const n2 = await workflow.createNode({
+ type: 'response-message',
+ config: {
+ message: 'm2',
+ },
+ upstreamId: n1.id,
+ });
+ await n1.setDownstream(n2);
+
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body).toMatchObject({ data: { title: 't1' } });
+ expect(res1.body.messages).toEqual([{ message: 'm1' }, { message: 'm2' }]);
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeDefined();
+ expect(post.title).toBe('t1');
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(2);
+ });
+
+ it('single dynamic message', async () => {
+ const n1 = await workflow.createNode({
+ type: 'response-message',
+ config: {
+ message: 'new post "{{ $context.params.values.title }}" by {{ $context.user.nickname }}',
+ },
+ });
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body).toMatchObject({ data: { title: 't1' } });
+ expect(res1.body.messages).toEqual([{ message: `new post "t1" by ${users[0].nickname}` }]);
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeDefined();
+ expect(post.title).toBe('t1');
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(1);
+ });
+ });
+
+ describe('end as success', () => {
+ it('no message', async () => {
+ const n1 = await workflow.createNode({
+ type: 'end',
+ config: {
+ endStatus: JOB_STATUS.RESOLVED,
+ },
+ });
+
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body.data).toBeUndefined();
+ expect(res1.body.messages).toBeUndefined();
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeNull();
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(1);
+
+ const posts = await PostRepo.find();
+ expect(posts.length).toBe(0);
+ });
+
+ it('single static message', async () => {
+ const n1 = await workflow.createNode({
+ type: 'response-message',
+ config: {
+ message: 'm1',
+ },
+ });
+
+ const n2 = await workflow.createNode({
+ type: 'end',
+ config: {
+ endStatus: JOB_STATUS.RESOLVED,
+ },
+ upstreamId: n1.id,
+ });
+
+ await n1.setDownstream(n2);
+
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body.data).toBeUndefined();
+ expect(res1.body.messages).toEqual([{ message: 'm1' }]);
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeNull();
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(2);
+ });
+ });
+
+ describe('end as failure', () => {
+ it('no message', async () => {
+ const n1 = await workflow.createNode({
+ type: 'end',
+ config: {
+ endStatus: JOB_STATUS.FAILED,
+ },
+ });
+
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(400);
+ expect(res1.body.data).toBeUndefined();
+ expect(res1.body.messages).toBeUndefined();
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeNull();
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.FAILED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(1);
+ });
+
+ it('single static message', async () => {
+ const n1 = await workflow.createNode({
+ type: 'response-message',
+ config: {
+ message: 'm1',
+ },
+ });
+
+ const n2 = await workflow.createNode({
+ type: 'end',
+ config: {
+ endStatus: JOB_STATUS.FAILED,
+ },
+ upstreamId: n1.id,
+ });
+
+ await n1.setDownstream(n2);
+
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ });
+
+ expect(res1.status).toBe(400);
+ expect(res1.body.data).toBeUndefined();
+ expect(res1.body.errors).toEqual([{ message: 'm1' }]);
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeNull();
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.FAILED);
+ const jobs = await e1.getJobs();
+ expect(jobs.length).toBe(2);
+ });
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/multiple.test.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/multiple.test.ts
new file mode 100644
index 0000000000..21f207c449
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/__tests__/multiple.test.ts
@@ -0,0 +1,185 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+import Database from '@nocobase/database';
+import { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow';
+import { getApp } from '@nocobase/plugin-workflow-test';
+import { MockServer } from '@nocobase/test';
+
+import Plugin from '..';
+
+describe.skip('workflow > multiple workflows', () => {
+ let app: MockServer;
+ let db: Database;
+ let PostRepo;
+ let WorkflowModel;
+ let workflow;
+ let users;
+ let userAgents;
+
+ beforeEach(async () => {
+ app = await getApp({
+ plugins: ['users', 'auth', 'error-handler', 'workflow-request-interceptor', Plugin],
+ });
+
+ db = app.db;
+
+ PostRepo = db.getCollection('posts').repository;
+
+ WorkflowModel = db.getModel('workflows');
+ workflow = await WorkflowModel.create({
+ enabled: true,
+ type: 'request-interception',
+ config: {
+ global: true,
+ actions: ['create'],
+ collection: 'posts',
+ },
+ });
+
+ const UserModel = db.getCollection('users').model;
+ users = await UserModel.bulkCreate([
+ { id: 2, nickname: 'a' },
+ { id: 3, nickname: 'b' },
+ ]);
+
+ userAgents = await Promise.all(users.map((user) => app.agent().login(user)));
+ });
+
+ afterEach(() => app.destroy());
+
+ describe('order', () => {
+ it('workflow 2 run first and pass, workflow 1 ends as success', async () => {
+ const n1 = await workflow.createNode({
+ type: 'response-message',
+ config: {
+ message: 'm1',
+ },
+ });
+ const n2 = await workflow.createNode({
+ type: 'end',
+ config: {
+ endStatus: JOB_STATUS.RESOLVED,
+ },
+ upstreamId: n1.id,
+ });
+ await n1.setDownstream(n2);
+
+ const w2 = await WorkflowModel.create({
+ enabled: true,
+ type: 'request-interception',
+ config: {
+ action: 'create',
+ collection: 'posts',
+ },
+ });
+
+ const n3 = await w2.createNode({
+ type: 'response-message',
+ config: {
+ message: 'm2',
+ },
+ });
+
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ triggerWorkflows: w2.key,
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body.data).toBeUndefined();
+ expect(res1.body.messages).toEqual([{ message: 'm2' }, { message: 'm1' }]);
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeNull();
+
+ const [e1] = await workflow.getExecutions();
+ expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const j1s = await e1.getJobs();
+ expect(j1s.length).toBe(2);
+
+ const [e2] = await w2.getExecutions();
+ expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const j2s = await e2.getJobs();
+ expect(j2s.length).toBe(1);
+ });
+
+ it('local workflow in trigger key order', async () => {
+ const w1 = await WorkflowModel.create({
+ enabled: true,
+ type: 'request-interception',
+ config: {
+ action: 'create',
+ collection: 'posts',
+ },
+ });
+
+ const n1 = await w1.createNode({
+ type: 'response-message',
+ config: {
+ message: 'm1',
+ },
+ });
+
+ const w2 = await WorkflowModel.create({
+ enabled: true,
+ type: 'request-interception',
+ config: {
+ action: 'create',
+ collection: 'posts',
+ },
+ });
+
+ const n2 = await w2.createNode({
+ type: 'response-message',
+ config: {
+ message: 'm2',
+ },
+ });
+
+ const n3 = await w2.createNode({
+ type: 'end',
+ config: {
+ endStatus: JOB_STATUS.RESOLVED,
+ },
+ upstreamId: n2.id,
+ });
+
+ await n2.setDownstream(n3);
+
+ const res1 = await userAgents[0].resource('posts').create({
+ values: { title: 't1' },
+ triggerWorkflows: [w2.key, w1.key].join(),
+ });
+
+ expect(res1.status).toBe(200);
+ expect(res1.body.data).toBeUndefined();
+ expect(res1.body.messages).toEqual([{ message: 'm2' }]);
+
+ const post = await PostRepo.findOne();
+ expect(post).toBeNull();
+
+ const e1s = await w1.getExecutions();
+ expect(e1s.length).toBe(0);
+ const [e2] = await w2.getExecutions();
+ expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED);
+ const jobs = await e2.getJobs();
+ expect(jobs.length).toBe(2);
+ });
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/index.ts b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/index.ts
new file mode 100644
index 0000000000..b0c269d075
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow-response-message/src/server/index.ts
@@ -0,0 +1,19 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This program is offered under a commercial license.
+ * For more information, see
+ */
+
+export { default } from './Plugin';
diff --git a/packages/plugins/@nocobase/plugin-workflow-sql/package.json b/packages/plugins/@nocobase/plugin-workflow-sql/package.json
index a544982da0..004ffd5fef 100644
--- a/packages/plugins/@nocobase/plugin-workflow-sql/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-sql/package.json
@@ -4,7 +4,7 @@
"displayName.zh-CN": "工作流:SQL 节点",
"description": "Execute SQL statements in workflow.",
"description.zh-CN": "可用于在工作流中对数据库执行任意 SQL 语句。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-sql",
diff --git a/packages/plugins/@nocobase/plugin-workflow-test/package.json b/packages/plugins/@nocobase/plugin-workflow-test/package.json
index 61aed27ca1..74af5a3c0d 100644
--- a/packages/plugins/@nocobase/plugin-workflow-test/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow-test/package.json
@@ -2,7 +2,7 @@
"name": "@nocobase/plugin-workflow-test",
"displayName": "Workflow: test kit",
"displayName.zh-CN": "工作流:测试工具包",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "dist/server/index.js",
"types": "./dist/server/index.d.ts",
diff --git a/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2ePageObjectModel.ts b/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2ePageObjectModel.ts
index 07a2a20e4f..dfe589300b 100644
--- a/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2ePageObjectModel.ts
+++ b/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2ePageObjectModel.ts
@@ -189,7 +189,7 @@ export class ApprovalPassthroughModeNode {
this.detailsConfigureFieldsButton = page.getByLabel(
`schema-initializer-Grid-details:configureFields-${collectionName}`,
);
- this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' }).getByRole('switch');
+ this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' });
this.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
this.actionsConfigureActionsButton = page.getByLabel(
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-',
@@ -262,7 +262,7 @@ export class ApprovalBranchModeNode {
this.detailsConfigureFieldsButton = page.getByLabel(
`schema-initializer-Grid-details:configureFields-${collectionName}`,
);
- this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' }).getByRole('switch');
+ this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' });
this.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
this.actionsConfigureActionsButton = page.getByLabel('schema-initializer-ActionBar-');
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');
diff --git a/packages/plugins/@nocobase/plugin-workflow/package.json b/packages/plugins/@nocobase/plugin-workflow/package.json
index f18984030b..e1b5002f46 100644
--- a/packages/plugins/@nocobase/plugin-workflow/package.json
+++ b/packages/plugins/@nocobase/plugin-workflow/package.json
@@ -4,13 +4,13 @@
"displayName.zh-CN": "工作流",
"description": "A powerful BPM tool that provides foundational support for business automation, with the capability to extend unlimited triggers and nodes.",
"description.zh-CN": "一个强大的 BPM 工具,为业务自动化提供基础支持,并且可任意扩展更多的触发器和节点。",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/workflow",
"dependencies": {
- "@nocobase/plugin-workflow-test": "1.7.0-alpha.10"
+ "@nocobase/plugin-workflow-test": "1.7.0-alpha.11"
},
"devDependencies": {
"@ant-design/icons": "5.x",
@@ -24,6 +24,7 @@
"dayjs": "^1.11.8",
"lodash": "4.17.21",
"lru-cache": "8.0.5",
+ "nodejs-snowflake": "2.0.1",
"react": "18.x",
"react-i18next": "^11.15.1",
"react-js-cron": "^3.1.0",
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowPane.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowPane.tsx
index 8bbec984af..df95f309b5 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowPane.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowPane.tsx
@@ -63,11 +63,10 @@ function SyncOptionSelect(props) {
if (trigger.sync != null) {
field.setValue(trigger.sync);
} else {
- field.setInitialValue(false);
+ field.setInitialValue(props.value ?? false);
}
}
- }, [record.id, field, workflowPlugin.triggers]);
-
+ }, [record.id, field, workflowPlugin.triggers, record.type, props.value]);
return ;
}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowTasks.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowTasks.tsx
index 6d0f60a1a0..17d435fa26 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowTasks.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowTasks.tsx
@@ -11,14 +11,17 @@ import { PageHeader } from '@ant-design/pro-layout';
import { Badge, Button, Layout, Menu, Tabs, Tooltip } from 'antd';
import classnames from 'classnames';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
-import { Link, Outlet, useNavigate, useParams } from 'react-router-dom';
+import { Link, useNavigate, useParams } from 'react-router-dom';
import {
+ ActionContextProvider,
+ CollectionRecordProvider,
css,
PinnedPluginListProvider,
SchemaComponent,
SchemaComponentContext,
SchemaComponentOptions,
+ useAPIClient,
useApp,
useCompile,
useDocumentTitle,
@@ -44,9 +47,11 @@ const contentClass = css`
export interface TaskTypeOptions {
title: string;
collection: string;
+ action: string;
useActionParams: Function;
- component: React.ComponentType;
- extraActions?: React.ComponentType;
+ Actions?: React.ComponentType;
+ Item: React.ComponentType;
+ Detail: React.ComponentType;
// children?: TaskTypeOptions[];
}
@@ -65,7 +70,7 @@ function MenuLink({ type }: any) {
return (
,
+ right: ,
}
: {}
}
@@ -157,16 +162,45 @@ function useCurrentTaskType() {
);
}
+function PopupContext(props: any) {
+ const { popupId } = useParams();
+ const { record } = usePopupRecordContext();
+ const navigate = useNavigate();
+ if (!popupId) {
+ return null;
+ }
+ return (
+ {
+ if (!visible) {
+ navigate(-1);
+ }
+ }}
+ openMode="modal"
+ >
+ {props.children}
+
+ );
+}
+
+const PopupRecordContext = createContext({ record: null, setRecord: (record) => {} });
+export function usePopupRecordContext() {
+ return useContext(PopupRecordContext);
+}
+
export function WorkflowTasks() {
const compile = useCompile();
const { setTitle } = useDocumentTitle();
const navigate = useNavigate();
- const { taskType, status = TASK_STATUS.PENDING } = useParams();
+ const apiClient = useAPIClient();
+ const { taskType, status = TASK_STATUS.PENDING, popupId } = useParams();
const { token } = useToken();
+ const [currentRecord, setCurrentRecord] = useState(null);
const items = useTaskTypeItems();
- const { title, collection, useActionParams, component: Component } = useCurrentTaskType();
+ const { title, collection, action = 'list', useActionParams, Item, Detail } = useCurrentTaskType();
const params = useActionParams(status);
@@ -180,6 +214,24 @@ export function WorkflowTasks() {
}
}, [items, navigate, status, taskType]);
+ useEffect(() => {
+ if (popupId && !currentRecord) {
+ apiClient
+ .resource(collection)
+ .get({
+ filterByTk: popupId,
+ })
+ .then((res) => {
+ if (res.data?.data) {
+ setCurrentRecord(res.data.data);
+ }
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ }
+ }, [popupId, collection, currentRecord, apiClient]);
+
const typeKey = taskType ?? items[0].key;
return (
@@ -205,84 +257,95 @@ export function WorkflowTasks() {
}
`}
>
-
-
+
+ .itemCss:not(:last-child) {
- border-bottom: none;
- }
- `,
- locale: {
- emptyText: `{{ t("No data yet", { ns: "${NAMESPACE}" }) }}`,
- },
+ properties: {
+ header: {
+ type: 'void',
+ 'x-component': 'PageHeader',
+ 'x-component-props': {
+ className: classnames('pageHeaderCss'),
+ style: {
+ background: token.colorBgContainer,
+ padding: '12px 24px 0 24px',
},
- properties: {
- item: {
- type: 'object',
- 'x-decorator': 'List.Item',
- 'x-component': Component,
- 'x-read-pretty': true,
+ title,
+ },
+ properties: {
+ tabs: {
+ type: 'void',
+ 'x-component': 'StatusTabs',
+ },
+ },
+ },
+ content: {
+ type: 'void',
+ 'x-component': 'Layout.Content',
+ 'x-component-props': {
+ className: contentClass,
+ style: {
+ padding: `${token.paddingPageVertical}px ${token.paddingPageHorizontal}px`,
+ },
+ },
+ properties: {
+ list: {
+ type: 'array',
+ 'x-component': 'List',
+ 'x-component-props': {
+ className: css`
+ > .itemCss:not(:last-child) {
+ border-bottom: none;
+ }
+ `,
+ locale: {
+ emptyText: `{{ t("No data yet", { ns: "${NAMESPACE}" }) }}`,
+ },
+ },
+ properties: {
+ item: {
+ type: 'object',
+ 'x-decorator': 'List.Item',
+ 'x-component': Item,
+ 'x-read-pretty': true,
+ },
},
},
},
},
+ popup: {
+ type: 'void',
+ 'x-decorator': PopupContext,
+ 'x-component': Detail,
+ },
},
- },
- }}
- />
-
-
+ }}
+ />
+
+
);
@@ -296,7 +359,7 @@ function WorkflowTasksLink() {
return types.length ? (
-
+
@@ -357,7 +420,7 @@ function TasksCountsProvider(props: any) {
return {props.children} ;
}
-export const TasksProvider = (props: any) => {
+export function TasksProvider(props: any) {
const isLoggedIn = useIsLoggedIn();
const content = (
@@ -377,4 +440,4 @@ export const TasksProvider = (props: any) => {
);
return isLoggedIn ? {content} : content;
-};
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/TriggerCollectionRecordSelect.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/TriggerCollectionRecordSelect.tsx
index ca1f085b11..42d964426c 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/TriggerCollectionRecordSelect.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/TriggerCollectionRecordSelect.tsx
@@ -21,19 +21,19 @@ export function TriggerCollectionRecordSelect(props) {
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
const { collectionManager } = app.dataSourceManager.getDataSource(dataSourceName);
const collection = collectionManager.getCollection(collectionName);
- const render = (props) => (
+ const render = (p) => (
);
return (
@@ -42,6 +42,7 @@ export function TriggerCollectionRecordSelect(props) {
onChange={props.onChange}
nullable={false}
changeOnSelect
+ {...props}
render={render}
/>
);
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx
index f17a548b3a..893f493509 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx
@@ -120,15 +120,10 @@ export default class PluginWorkflowClient extends Plugin {
});
this.router.add('admin.workflow.tasks', {
- path: '/admin/workflow/tasks/:taskType/:status?',
+ path: '/admin/workflow/tasks/:taskType/:status/:popupId?',
Component: WorkflowTasks,
});
- this.router.add('admin.workflow.tasks.popup', {
- path: '/admin/workflow/tasks/:taskType/:status/popups/*',
- Component: PagePopups,
- });
-
this.app.pluginSettingsManager.add(NAMESPACE, {
icon: 'PartitionOutlined',
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
@@ -193,3 +188,4 @@ export { default as useStyles } from './style';
export { Trigger, useTrigger } from './triggers';
export * from './utils';
export * from './variable';
+export { TASK_STATUS, usePopupRecordContext } from './WorkflowTasks';
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/RepeatField.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/RepeatField.tsx
index 206f8b273a..9024a5a614 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/RepeatField.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/RepeatField.tsx
@@ -42,7 +42,7 @@ function getRepeatTypeValue(v) {
return 'none';
}
-function CommonRepeatField({ value, onChange }) {
+function CommonRepeatField({ value, onChange, disabled }) {
const { t } = useWorkflowTranslation();
const option = getNumberOption(value);
@@ -59,11 +59,12 @@ function CommonRepeatField({ value, onChange }) {
addonBefore={t('Every')}
addonAfter={t(option.unitText)}
className="auto-width"
+ disabled={disabled}
/>
);
}
-export function RepeatField({ value = null, onChange }) {
+export function RepeatField({ value = null, onChange, disabled }) {
const { t } = useWorkflowTranslation();
const typeValue = getRepeatTypeValue(value);
const onTypeChange = useCallback(
@@ -114,20 +115,23 @@ export function RepeatField({ value = null, onChange }) {
}
`}
>
-
+
{RepeatOptions.map((item) => (
{t(item.text)}
))}
- {typeof typeValue === 'number' ? : null}
+ {typeof typeValue === 'number' ? (
+
+ ) : null}
{typeValue === 'cron' ? (
onChange(`0 ${v}`)}
clearButton={false}
locale={window['cronLocale']}
+ disabled={disabled}
/>
) : null}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx
index 5180e9eafd..ff5a14e951 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx
@@ -133,20 +133,8 @@ export const systemOptions = {
export const BaseTypeSets = {
boolean: new Set(['checkbox']),
number: new Set(['integer', 'number', 'percent']),
- string: new Set([
- 'input',
- 'password',
- 'email',
- 'phone',
- 'select',
- 'radioGroup',
- 'text',
- 'markdown',
- 'richText',
- 'expression',
- 'time',
- ]),
- date: new Set(['date', 'createdAt', 'updatedAt']),
+ string: new Set(['input', 'password', 'email', 'phone', 'select', 'radioGroup', 'text', 'markdown', 'richText']),
+ date: new Set(['datetime', 'datetimeNoTz', 'dateOnly', 'createdAt', 'updatedAt']),
};
// { type: 'reference', options: { collection: 'users', multiple: false } }
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/common/collections/jobs.ts b/packages/plugins/@nocobase/plugin-workflow/src/common/collections/jobs.ts
index 4b961d6801..9d5eb9028d 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/common/collections/jobs.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/common/collections/jobs.ts
@@ -14,7 +14,14 @@ export default {
migrationRules: ['schema-only'],
name: 'jobs',
shared: true,
+ autoGenId: false,
fields: [
+ {
+ type: 'bigInt',
+ name: 'id',
+ primaryKey: true,
+ autoIncrement: false,
+ },
{
type: 'belongsTo',
name: 'execution',
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Dispatcher.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Dispatcher.ts
new file mode 100644
index 0000000000..bd93f359c9
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Dispatcher.ts
@@ -0,0 +1,12 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+export default class Dispatcher {
+ constructor() {}
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts
index d45449c56a..2fe93695aa 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts
@@ -10,6 +10,7 @@
import path from 'path';
import { randomUUID } from 'crypto';
+import { Snowflake } from 'nodejs-snowflake';
import { Transaction, Transactionable } from 'sequelize';
import LRUCache from 'lru-cache';
@@ -61,6 +62,7 @@ export default class PluginWorkflowServer extends Plugin {
triggers: Registry = new Registry();
functions: Registry = new Registry();
enabledCache: Map = new Map();
+ snowflake: Snowflake;
private ready = false;
private executing: Promise | null = null;
@@ -111,6 +113,99 @@ export default class PluginWorkflowServer extends Plugin {
}
};
+ private onAfterCreate = async (model: WorkflowModel, { transaction }) => {
+ const WorkflowStatsModel = this.db.getModel('workflowStats');
+ let stats = await WorkflowStatsModel.findOne({
+ where: { key: model.key },
+ transaction,
+ });
+ if (!stats) {
+ stats = await model.createStats({ executed: 0 }, { transaction });
+ }
+ model.stats = stats;
+ model.versionStats = await model.createVersionStats({ id: model.id }, { transaction });
+ if (model.enabled) {
+ this.toggle(model, true, { transaction });
+ }
+ };
+
+ private onAfterUpdate = async (model: WorkflowModel, { transaction }) => {
+ model.stats = await model.getStats({ transaction });
+ model.versionStats = await model.getVersionStats({ transaction });
+ this.toggle(model, model.enabled, { transaction });
+ };
+
+ private onAfterDestroy = async (model: WorkflowModel, { transaction }) => {
+ this.toggle(model, false, { transaction });
+
+ const TaskRepo = this.db.getRepository('workflowTasks');
+ await TaskRepo.destroy({
+ filter: {
+ workflowId: model.id,
+ },
+ transaction,
+ });
+ };
+
+ // [Life Cycle]:
+ // * load all workflows in db
+ // * add all hooks for enabled workflows
+ // * add hooks for create/update[enabled]/delete workflow to add/remove specific hooks
+ private onAfterStart = async () => {
+ this.ready = true;
+
+ const collection = this.db.getCollection('workflows');
+ const workflows = await collection.repository.find({
+ filter: { enabled: true },
+ appends: ['stats', 'versionStats'],
+ });
+
+ for (const workflow of workflows) {
+ // NOTE: workflow stats may not be created in migration (for compatibility)
+ if (!workflow.stats) {
+ workflow.stats = await workflow.createStats({ executed: 0 });
+ }
+ // NOTE: workflow stats may not be created in migration (for compatibility)
+ if (!workflow.versionStats) {
+ workflow.versionStats = await workflow.createVersionStats({ executed: 0 });
+ }
+
+ this.toggle(workflow, true, { silent: true });
+ }
+
+ this.checker = setInterval(() => {
+ this.getLogger('dispatcher').info(`(cycling) check for queueing executions`);
+ this.dispatch();
+ }, 300_000);
+
+ this.app.on('workflow:dispatch', () => {
+ this.app.logger.info('workflow:dispatch');
+ this.dispatch();
+ });
+
+ // check for queueing executions
+ this.getLogger('dispatcher').info('(starting) check for queueing executions');
+ this.dispatch();
+ };
+
+ private onBeforeStop = async () => {
+ for (const workflow of this.enabledCache.values()) {
+ this.toggle(workflow, false, { silent: true });
+ }
+
+ this.ready = false;
+ if (this.events.length) {
+ await this.prepare();
+ }
+ if (this.executing) {
+ await this.executing;
+ }
+
+ if (this.checker) {
+ clearInterval(this.checker);
+ }
+ };
+
async handleSyncMessage(message) {
if (message.type === 'statusChange') {
if (message.enabled) {
@@ -219,6 +314,14 @@ export default class PluginWorkflowServer extends Plugin {
WorkflowRepository,
WorkflowTasksRepository,
});
+
+ const PluginRepo = this.db.getRepository('applicationPlugins');
+ const pluginRecord = await PluginRepo.findOne({
+ filter: { name: this.name },
+ });
+ this.snowflake = new Snowflake({
+ custom_epoch: pluginRecord?.createdAt.getTime(),
+ });
}
/**
@@ -279,84 +382,12 @@ export default class PluginWorkflowServer extends Plugin {
});
db.on('workflows.beforeSave', this.onBeforeSave);
- db.on('workflows.afterCreate', async (model: WorkflowModel, { transaction }) => {
- const WorkflowStatsModel = this.db.getModel('workflowStats');
- const [stats, created] = await WorkflowStatsModel.findOrCreate({
- where: { key: model.key },
- defaults: { key: model.key },
- transaction,
- });
- model.stats = stats;
- model.versionStats = await model.createVersionStats({ id: model.id }, { transaction });
- if (model.enabled) {
- this.toggle(model, true, { transaction });
- }
- });
- db.on('workflows.afterUpdate', async (model: WorkflowModel, { transaction }) => {
- model.stats = await model.getStats({ transaction });
- model.versionStats = await model.getVersionStats({ transaction });
- this.toggle(model, model.enabled, { transaction });
- });
- db.on('workflows.afterDestroy', async (model: WorkflowModel, { transaction }) => {
- this.toggle(model, false, { transaction });
+ db.on('workflows.afterCreate', this.onAfterCreate);
+ db.on('workflows.afterUpdate', this.onAfterUpdate);
+ db.on('workflows.afterDestroy', this.onAfterDestroy);
- const TaskRepo = this.db.getRepository('workflowTasks');
- await TaskRepo.destroy({
- filter: {
- workflowId: model.id,
- },
- transaction,
- });
- });
-
- // [Life Cycle]:
- // * load all workflows in db
- // * add all hooks for enabled workflows
- // * add hooks for create/update[enabled]/delete workflow to add/remove specific hooks
- this.app.on('afterStart', async () => {
- this.ready = true;
-
- const collection = db.getCollection('workflows');
- const workflows = await collection.repository.find({
- filter: { enabled: true },
- });
-
- workflows.forEach((workflow: WorkflowModel) => {
- this.toggle(workflow, true, { silent: true });
- });
-
- this.checker = setInterval(() => {
- this.getLogger('dispatcher').info(`(cycling) check for queueing executions`);
- this.dispatch();
- }, 300_000);
-
- this.app.on('workflow:dispatch', () => {
- this.app.logger.info('workflow:dispatch');
- this.dispatch();
- });
-
- // check for queueing executions
- this.getLogger('dispatcher').info('(starting) check for queueing executions');
- this.dispatch();
- });
-
- this.app.on('beforeStop', async () => {
- for (const workflow of this.enabledCache.values()) {
- this.toggle(workflow, false, { silent: true });
- }
-
- this.ready = false;
- if (this.events.length) {
- await this.prepare();
- }
- if (this.executing) {
- await this.executing;
- }
-
- if (this.checker) {
- clearInterval(this.checker);
- }
- });
+ this.app.on('afterStart', this.onAfterStart);
+ this.app.on('beforeStop', this.onBeforeStop);
}
private toggle(
@@ -376,11 +407,16 @@ export default class PluginWorkflowServer extends Plugin {
const prev = workflow.previous();
if (prev.config) {
trigger.off({ ...workflow.get(), ...prev });
+ this.getLogger(workflow.id).info(`toggle OFF workflow ${workflow.id} based on configuration before updated`);
}
trigger.on(workflow);
+ this.getLogger(workflow.id).info(`toggle ON workflow ${workflow.id}`);
+
this.enabledCache.set(workflow.id, workflow);
} else {
trigger.off(workflow);
+ this.getLogger(workflow.id).info(`toggle OFF workflow ${workflow.id}`);
+
this.enabledCache.delete(workflow.id);
}
if (!silent) {
@@ -759,10 +795,16 @@ export default class PluginWorkflowServer extends Plugin {
/**
* @experimental
*/
- public async toggleTaskStatus(task: WorkflowTaskModel, done: boolean, { transaction }: Transactionable) {
+ public async toggleTaskStatus(task: WorkflowTaskModel, on: boolean, { transaction }: Transactionable) {
const { db } = this.app;
const repository = db.getRepository('workflowTasks') as WorkflowTasksRepository;
- if (done) {
+ if (on) {
+ await repository.updateOrCreate({
+ filterKeys: ['key', 'type'],
+ values: task,
+ transaction,
+ });
+ } else {
await repository.destroy({
filter: {
type: task.type,
@@ -770,12 +812,6 @@ export default class PluginWorkflowServer extends Plugin {
},
transaction,
});
- } else {
- await repository.updateOrCreate({
- filterKeys: ['key', 'type'],
- values: task,
- transaction,
- });
}
// NOTE:
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts
index 1a6d20dd79..f140767363 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts
@@ -56,15 +56,9 @@ export default class Processor {
*/
nodesMap = new Map();
- /**
- * @experimental
- */
- jobsMap = new Map();
-
- /**
- * @experimental
- */
- jobsMapByNodeKey: { [key: string]: any } = {};
+ private jobsMapByNodeKey: { [key: string]: JobModel } = {};
+ private jobResultsMapByNodeKey: { [key: string]: any } = {};
+ private jobsToSave: Map = new Map();
/**
* @experimental
@@ -100,10 +94,9 @@ export default class Processor {
private makeJobs(jobs: Array) {
jobs.forEach((job) => {
- this.jobsMap.set(job.id, job);
-
const node = this.nodesMap.get(job.nodeId);
- this.jobsMapByNodeKey[node.key] = job.result;
+ this.jobsMapByNodeKey[node.key] = job;
+ this.jobResultsMapByNodeKey[node.key] = job.result;
});
}
@@ -192,11 +185,11 @@ export default class Processor {
}
if (!(job instanceof Model)) {
- job.upstreamId = prevJob instanceof Model ? prevJob.get('id') : null;
+ // job.upstreamId = prevJob instanceof Model ? prevJob.get('id') : null;
job.nodeId = node.id;
job.nodeKey = node.key;
}
- const savedJob = await this.saveJob(job);
+ const savedJob = this.saveJob(job);
this.logger.info(
`execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) finished as status: ${savedJob.status}`,
@@ -258,6 +251,30 @@ export default class Processor {
}
public async exit(s?: number) {
+ if (this.jobsToSave.size) {
+ const newJobs = [];
+ for (const job of this.jobsToSave.values()) {
+ if (job.isNewRecord) {
+ newJobs.push(job);
+ } else {
+ await job.save({ transaction: this.mainTransaction });
+ }
+ }
+ if (newJobs.length) {
+ const JobsModel = this.options.plugin.db.getModel('jobs');
+ await JobsModel.bulkCreate(
+ newJobs.map((job) => job.toJSON()),
+ {
+ transaction: this.mainTransaction,
+ returning: false,
+ },
+ );
+ for (const job of newJobs) {
+ job.isNewRecord = false;
+ }
+ }
+ this.jobsToSave.clear();
+ }
if (typeof s === 'number') {
const status = (this.constructor).StatusMap[s] ?? Math.sign(s);
await this.execution.update({ status }, { transaction: this.mainTransaction });
@@ -269,33 +286,30 @@ export default class Processor {
return null;
}
- // TODO(optimize)
/**
* @experimental
*/
- async saveJob(payload: JobModel | Record): Promise {
+ saveJob(payload: JobModel | Record): JobModel {
const { database } = this.execution.constructor;
- const { mainTransaction: transaction } = this;
const { model } = database.getCollection('jobs');
let job;
if (payload instanceof model) {
- job = await payload.save({ transaction });
- } else if (payload.id) {
- job = await model.findByPk(payload.id, { transaction });
- await job.update(payload, { transaction });
+ job = payload;
+ job.set('updatedAt', new Date());
} else {
- job = await model.create(
- {
- ...payload,
- executionId: this.execution.id,
- },
- { transaction },
- );
+ job = model.build({
+ ...payload,
+ id: this.options.plugin.snowflake.getUniqueID().toString(),
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ executionId: this.execution.id,
+ });
}
- this.jobsMap.set(job.id, job);
+ this.jobsToSave.set(job.id, job);
this.lastSavedJob = job;
- this.jobsMapByNodeKey[job.nodeKey] = job.result;
+ this.jobsMapByNodeKey[job.nodeKey] = job;
+ this.jobResultsMapByNodeKey[job.nodeKey] = job.result;
return job;
}
@@ -357,32 +371,20 @@ export default class Processor {
* @experimental
*/
findBranchParentJob(job: JobModel, node: FlowNodeModel): JobModel | null {
- for (let j: JobModel | undefined = job; j; j = this.jobsMap.get(j.upstreamId)) {
- if (j.nodeId === node.id) {
- return j;
- }
- }
- return null;
+ return this.jobsMapByNodeKey[node.key];
}
/**
* @experimental
*/
findBranchLastJob(node: FlowNodeModel, job: JobModel): JobModel | null {
- const allJobs = Array.from(this.jobsMap.values());
+ const allJobs = Object.values(this.jobsMapByNodeKey);
const branchJobs = [];
for (let n = this.findBranchEndNode(node); n && n !== node.upstream; n = n.upstream) {
branchJobs.push(...allJobs.filter((item) => item.nodeId === n.id));
}
- branchJobs.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
- for (let i = branchJobs.length - 1; i >= 0; i -= 1) {
- for (let j = branchJobs[i]; j && j.id !== job.id; j = this.jobsMap.get(j.upstreamId)) {
- if (j.upstreamId === job.id) {
- return branchJobs[i];
- }
- }
- }
- return null;
+ branchJobs.sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime());
+ return branchJobs[branchJobs.length - 1] || null;
}
/**
@@ -403,13 +405,13 @@ export default class Processor {
for (let n = includeSelfScope ? node : this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) {
const instruction = this.options.plugin.instructions.get(n.type);
if (typeof instruction?.getScope === 'function') {
- $scopes[n.id] = $scopes[n.key] = instruction.getScope(n, this.jobsMapByNodeKey[n.key], this);
+ $scopes[n.id] = $scopes[n.key] = instruction.getScope(n, this.jobResultsMapByNodeKey[n.key], this);
}
}
return {
$context: this.execution.context,
- $jobsMapByNodeKey: this.jobsMapByNodeKey,
+ $jobsMapByNodeKey: this.jobResultsMapByNodeKey,
$system: systemFns,
$scopes,
$env: this.options.plugin.app.environment.getVariables(),
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Plugin.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Plugin.test.ts
index acc0344163..44ffe8c985 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Plugin.test.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Plugin.test.ts
@@ -43,6 +43,11 @@ describe('workflow > Plugin', () => {
});
expect(workflow.current).toBe(true);
+
+ expect(workflow.stats).toBeDefined();
+ expect(workflow.stats.executed).toBe(0);
+ expect(workflow.versionStats).toBeDefined();
+ expect(workflow.versionStats.executed).toBe(0);
});
it('create with disabled', async () => {
@@ -358,6 +363,10 @@ describe('workflow > Plugin', () => {
await sleep(500);
+ const w1_1 = plugin.enabledCache.get(w1.id);
+ expect(w1_1.stats).toBeDefined();
+ expect(w1_1.stats.executed).toBe(0);
+
await e1.reload();
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Processor.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Processor.test.ts
index 1acc1a0188..5a09c92452 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Processor.test.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Processor.test.ts
@@ -41,27 +41,19 @@ describe('workflow > Processor', () => {
afterEach(() => app.destroy());
describe('base', () => {
- it.skip('saveJob', async () => {
- const execution = await workflow.createExecution({
- key: workflow.key,
- context: {},
- status: EXECUTION_STATUS.STARTED,
- eventKey: '123',
- });
+ it.skipIf(process.env['DB_DIALECT'] === 'sqlite')('job id out of max safe integer', async () => {
+ const JobModel = db.getModel('jobs');
- const processor = plugin.createProcessor(execution);
+ const records = await JobModel.bulkCreate([
+ {
+ id: '10267424896650240',
+ },
+ {
+ id: '10267424930204672',
+ },
+ ]);
- const job1 = await processor.saveJob({
- status: JOB_STATUS.RESOLVED,
- result: null,
- });
-
- const job2 = await processor.saveJob({
- status: JOB_STATUS.RESOLVED,
- result: 'abc',
- });
-
- expect(job2).toBeDefined();
+ expect(records.length).toBe(2);
});
it('empty workflow without any nodes', async () => {
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/migrations/20250320223415-stats.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/migrations/20250320223415-stats.test.ts
index 9e373fc164..cec660e4ff 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/migrations/20250320223415-stats.test.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/migrations/20250320223415-stats.test.ts
@@ -26,7 +26,7 @@ describe('20250320223415-stats', () => {
describe('legacy stats should be migrated', () => {
beforeEach(async () => {
app = await getApp();
- app.version.update('1.6.0');
+ await app.version.update('1.6.0');
plugin = app.pm.get(PluginWorkflowServer) as PluginWorkflowServer;
WorkflowRepo = app.db.getRepository('workflows');
WorkflowStatsRepo = app.db.getRepository('workflowStats');
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/migrations/20250409164913-remove-jobs-auto-increment.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/migrations/20250409164913-remove-jobs-auto-increment.test.ts
new file mode 100644
index 0000000000..5710e3317a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/migrations/20250409164913-remove-jobs-auto-increment.test.ts
@@ -0,0 +1,64 @@
+/**
+ * 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 { MockServer } from '@nocobase/test';
+import { getApp } from '@nocobase/plugin-workflow-test';
+
+import Migration from '../../migrations/20250409164913-remove-jobs-auto-increment';
+
+describe.skipIf(process.env.DB_DIALECT === 'sqlite')('20250409164913-remove-jobs-auto-increment', () => {
+ let app: MockServer;
+ let OldCollection;
+
+ beforeEach(async () => {
+ app = await getApp();
+ await app.version.update('1.6.0');
+ OldCollection = app.db.collection({
+ name: 'jobs',
+ fields: [],
+ });
+ await app.db.sync({ force: true });
+ });
+
+ afterEach(() => app.destroy());
+
+ it.runIf(process.env.DB_DIALECT === 'postgres')('[PG] no auto increment any more', async () => {
+ const oldColumns = await app.db.sequelize.getQueryInterface().describeTable(OldCollection.getTableNameWithSchema());
+ expect(oldColumns.id.defaultValue.includes('jobs_id_seq::regclass')).toBe(true);
+
+ const JobRepo = app.db.getRepository('jobs');
+ const j1 = await JobRepo.create({});
+ expect(j1.id).toBe(1);
+
+ const migration = new Migration({ app, db: app.db } as any);
+ await migration.up();
+
+ const newColumns = await app.db.sequelize.getQueryInterface().describeTable(OldCollection.getTableNameWithSchema());
+ expect(newColumns.id.defaultValue).toBeFalsy();
+
+ await expect(async () => await JobRepo.create({})).rejects.toThrow();
+ });
+
+ it.runIf(['mysql', 'mariadb'].includes(process.env.DB_DIALECT))('[MySQL] no auto increment any more', async () => {
+ const oldColumns = await app.db.sequelize.getQueryInterface().describeTable(OldCollection.getTableNameWithSchema());
+ expect(oldColumns.id.autoIncrement).toBe(true);
+
+ const JobRepo = app.db.getRepository('jobs');
+ const j1 = await JobRepo.create({});
+ expect(j1.id).toBe(1);
+
+ const migration = new Migration({ app, db: app.db } as any);
+ await migration.up();
+
+ const newColumns = await app.db.sequelize.getQueryInterface().describeTable(OldCollection.getTableNameWithSchema());
+ expect(newColumns.id.autoIncrement).toBe(false);
+
+ await expect(async () => await JobRepo.create({})).rejects.toThrow();
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/triggers/collection.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/triggers/collection.test.ts
index f67b7a84e4..cff13e5d64 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/triggers/collection.test.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/triggers/collection.test.ts
@@ -359,6 +359,50 @@ describe('workflow > triggers > collection', () => {
const executions = await workflow.getExecutions();
expect(executions.length).toBe(1);
});
+
+ it('datetime field not changed', async () => {
+ const workflow = await WorkflowModel.create({
+ enabled: true,
+ sync: true,
+ type: 'collection',
+ config: {
+ mode: 2,
+ collection: 'posts',
+ changed: ['createdAt'],
+ },
+ });
+
+ const post = await PostRepo.create({ values: { title: 't1' } });
+ await PostRepo.update({ filterByTk: post.id, values: { ...post.get(), title: 't2' } });
+
+ const executions = await workflow.getExecutions();
+ expect(executions.length).toBe(0);
+ });
+
+ it('datetimeNoTz field not changed', async () => {
+ db.getCollection('posts').addField('dateOnly', {
+ type: 'datetimeNoTz',
+ });
+
+ await db.sync();
+
+ const workflow = await WorkflowModel.create({
+ enabled: true,
+ sync: true,
+ type: 'collection',
+ config: {
+ mode: 2,
+ collection: 'posts',
+ changed: ['dateOnly'],
+ },
+ });
+
+ const post = await PostRepo.create({ values: { title: 't1', dateOnly: '2020-01-01 00:00:00' } });
+ await PostRepo.update({ filterByTk: post.id, values: { ...post.get(), title: 't2' } });
+
+ const executions = await workflow.getExecutions();
+ expect(executions.length).toBe(0);
+ });
});
describe('config.condition', () => {
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20250320223415-stats.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20250320223415-stats.ts
index 0e0b787d09..45bbd3f81d 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20250320223415-stats.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20250320223415-stats.ts
@@ -26,16 +26,21 @@ export default class extends Migration {
const groupCounts: { [key: string]: { key: string; executed: number } } = {};
for (const workflow of workflows) {
- await WorkflowVersionStatsModel.findOrCreate({
+ const versionStats = await WorkflowVersionStatsModel.findOne({
where: {
id: workflow.id,
},
- defaults: {
- id: workflow.id,
- executed: workflow.get('executed'),
- },
transaction,
});
+ if (!versionStats) {
+ await WorkflowVersionStatsModel.create(
+ {
+ id: workflow.id,
+ executed: workflow.get('executed'),
+ },
+ { transaction },
+ );
+ }
const key = workflow.get('key');
groupCounts[key] = {
@@ -44,13 +49,15 @@ export default class extends Migration {
};
}
for (const values of Object.values(groupCounts)) {
- await WorkflowStatsModel.findOrCreate({
+ const stats = await WorkflowStatsModel.findOne({
where: {
key: values.key,
},
- defaults: values,
transaction,
});
+ if (!stats) {
+ await WorkflowStatsModel.create(values, { transaction });
+ }
}
});
}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20250409164913-remove-jobs-auto-increment.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20250409164913-remove-jobs-auto-increment.ts
new file mode 100644
index 0000000000..7795ba5bc5
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20250409164913-remove-jobs-auto-increment.ts
@@ -0,0 +1,37 @@
+/**
+ * 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 { Migration } from '@nocobase/server';
+
+export default class extends Migration {
+ appVersion = '<1.7.0';
+ on = 'beforeLoad';
+ async up() {
+ const { db } = this.context;
+ const jobCollection = db.collection({
+ name: 'jobs',
+ });
+ const tableNameWithQuotes = jobCollection.getRealTableName(true);
+
+ await db.sequelize.transaction(async (transaction) => {
+ if (this.db.isPostgresCompatibleDialect()) {
+ await db.sequelize.query(`ALTER TABLE ${tableNameWithQuotes} ALTER COLUMN id DROP DEFAULT`, {
+ transaction,
+ });
+ return;
+ }
+ if (this.db.isMySQLCompatibleDialect()) {
+ await db.sequelize.query(`ALTER TABLE ${tableNameWithQuotes} MODIFY COLUMN id BIGINT`, {
+ transaction,
+ });
+ return;
+ }
+ });
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.ts
index 8e7d6eba9d..3fe7020538 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.ts
@@ -104,50 +104,54 @@ export default class DateFieldScheduleTrigger {
// caching workflows in range, default to 5min
cacheCycle = 300_000;
+ onAfterStart = () => {
+ if (this.timer) {
+ return;
+ }
+
+ this.timer = setInterval(() => this.reload(), this.cacheCycle);
+
+ this.reload();
+ };
+
+ onBeforeStop = () => {
+ if (this.timer) {
+ clearInterval(this.timer);
+ }
+
+ for (const [key, timer] of this.cache.entries()) {
+ clearTimeout(timer);
+ this.cache.delete(key);
+ }
+ };
+
constructor(public workflow: Plugin) {
- workflow.app.on('afterStart', async () => {
- if (this.timer) {
- return;
- }
-
- this.timer = setInterval(() => this.reload(), this.cacheCycle);
-
- this.reload();
- });
-
- workflow.app.on('beforeStop', () => {
- if (this.timer) {
- clearInterval(this.timer);
- }
-
- for (const [key, timer] of this.cache.entries()) {
- clearTimeout(timer);
- this.cache.delete(key);
- }
- });
+ workflow.app.on('afterStart', this.onAfterStart);
+ workflow.app.on('beforeStop', this.onBeforeStop);
}
- async reload() {
+ reload() {
+ for (const [key, timer] of this.cache.entries()) {
+ clearTimeout(timer);
+ this.cache.delete(key);
+ }
+
const workflows = Array.from(this.workflow.enabledCache.values()).filter(
(item) => item.type === 'schedule' && item.config.mode === SCHEDULE_MODE.DATE_FIELD,
);
- // NOTE: clear cached jobs in last cycle
- this.cache = new Map();
-
- this.inspect(workflows);
+ workflows.forEach((workflow) => {
+ this.inspect(workflow);
+ });
}
- inspect(workflows: WorkflowModel[]) {
+ async inspect(workflow: WorkflowModel) {
const now = new Date();
-
- workflows.forEach(async (workflow) => {
- const records = await this.loadRecordsToSchedule(workflow, now);
- this.workflow.getLogger(workflow.id).info(`[Schedule on date field] ${records.length} records to schedule`);
- records.forEach((record) => {
- const nextTime = this.getRecordNextTime(workflow, record);
- this.schedule(workflow, record, nextTime, Boolean(nextTime));
- });
+ const records = await this.loadRecordsToSchedule(workflow, now);
+ this.workflow.getLogger(workflow.id).info(`[Schedule on date field] ${records.length} records to schedule`);
+ records.forEach((record) => {
+ const nextTime = this.getRecordNextTime(workflow, record);
+ this.schedule(workflow, record, nextTime, Boolean(nextTime));
});
}
@@ -233,8 +237,6 @@ export default class DateFieldScheduleTrigger {
[Op.gte]: new Date(endTimestamp),
},
});
- } else {
- this.workflow.getLogger(id).warn(`[Schedule on date field] "endsOn.field" is not configured`);
}
}
}
@@ -367,7 +369,7 @@ export default class DateFieldScheduleTrigger {
}
on(workflow: WorkflowModel) {
- this.inspect([workflow]);
+ this.inspect(workflow);
const { collection } = workflow.config;
const [dataSourceName, collectionName] = parseCollectionName(collection);
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/StaticScheduleTrigger.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/StaticScheduleTrigger.ts
index 542746ed9a..61d8a56d58 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/StaticScheduleTrigger.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/StaticScheduleTrigger.ts
@@ -18,36 +18,39 @@ const MAX_SAFE_INTERVAL = 2147483647;
export default class StaticScheduleTrigger {
private timers: Map = new Map();
- constructor(public workflow: Plugin) {
- workflow.app.on('afterStart', async () => {
- const workflows = Array.from(this.workflow.enabledCache.values()).filter(
- (item) => item.type === 'schedule' && item.config.mode === SCHEDULE_MODE.STATIC,
- );
-
- this.inspect(workflows);
- });
-
- workflow.app.on('beforeStop', () => {
- for (const timer of this.timers.values()) {
- clearInterval(timer);
- }
- });
- }
-
- inspect(workflows: WorkflowModel[]) {
- const now = new Date();
+ onAfterStart = () => {
+ const workflows = Array.from(this.workflow.enabledCache.values()).filter(
+ (item) => item.type === 'schedule' && item.config.mode === SCHEDULE_MODE.STATIC,
+ );
workflows.forEach((workflow) => {
- const nextTime = this.getNextTime(workflow, now);
- if (nextTime) {
- this.workflow
- .getLogger(workflow.id)
- .info(`caching scheduled workflow will run at: ${new Date(nextTime).toISOString()}`);
- } else {
- this.workflow.getLogger(workflow.id).info('workflow will not be scheduled');
- }
- this.schedule(workflow, nextTime, nextTime >= now.getTime());
+ this.inspect(workflow);
});
+ };
+
+ onBeforeStop = () => {
+ for (const timer of this.timers.values()) {
+ clearInterval(timer);
+ }
+ };
+
+ constructor(public workflow: Plugin) {
+ workflow.app.on('afterStart', this.onAfterStart);
+ workflow.app.on('beforeStop', this.onBeforeStop);
+ }
+
+ inspect(workflow: WorkflowModel) {
+ const now = new Date();
+
+ const nextTime = this.getNextTime(workflow, now);
+ if (nextTime) {
+ this.workflow
+ .getLogger(workflow.id)
+ .info(`caching scheduled workflow will run at: ${new Date(nextTime).toISOString()}`);
+ } else {
+ this.workflow.getLogger(workflow.id).info('workflow will not be scheduled');
+ }
+ this.schedule(workflow, nextTime, nextTime >= now.getTime());
}
getNextTime({ config, stats }: WorkflowModel, currentDate: Date, nextSecond = false) {
@@ -130,7 +133,7 @@ export default class StaticScheduleTrigger {
}
on(workflow) {
- this.inspect([workflow]);
+ this.inspect(workflow);
}
off(workflow) {
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/index.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/index.ts
index 375ef784ab..a2343e8fb2 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/index.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/index.ts
@@ -9,6 +9,7 @@
import Trigger from '..';
import type Plugin from '../../Plugin';
+import { WorkflowModel } from '../../types';
import DateFieldScheduleTrigger from './DateFieldScheduleTrigger';
import StaticScheduleTrigger from './StaticScheduleTrigger';
import { SCHEDULE_MODE } from './utils';
@@ -67,19 +68,15 @@ export default class ScheduleTrigger extends Trigger {
// return !existed.length;
// }
- validateContext(values) {
- if (!values?.mode) {
- return {
- mode: 'Mode is required',
- };
- }
- const trigger = this.getTrigger(values.mode);
+ validateContext(values, workflow: WorkflowModel) {
+ const { mode } = workflow.config;
+ const trigger = this.getTrigger(mode);
if (!trigger) {
return {
mode: 'Mode in invalid',
};
}
- return trigger.validateContext?.(values);
+ return trigger.validateContext?.(values, workflow);
}
}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/index.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/index.ts
index 24c01d874e..50e462fd33 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/index.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/index.ts
@@ -20,7 +20,7 @@ export abstract class Trigger {
return true;
}
duplicateConfig?(workflow: WorkflowModel, options: Transactionable): object | Promise;
- validateContext?(values: any): null | void | { [key: string]: string };
+ validateContext?(values: any, workflow: WorkflowModel): null | void | { [key: string]: string };
sync?: boolean;
execute?(
workflow: WorkflowModel,
diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json
index 478fb8fde1..bb336b51be 100644
--- a/packages/presets/nocobase/package.json
+++ b/packages/presets/nocobase/package.json
@@ -1,82 +1,86 @@
{
"name": "@nocobase/preset-nocobase",
- "version": "1.7.0-alpha.10",
+ "version": "1.7.0-alpha.11",
"license": "AGPL-3.0",
"main": "./lib/server/index.js",
"dependencies": {
"@formily/json-schema": "2.x",
- "@nocobase/json-template-parser": "1.7.0-alpha.10",
- "@nocobase/plugin-acl": "1.7.0-alpha.10",
- "@nocobase/plugin-action-bulk-edit": "1.7.0-alpha.10",
- "@nocobase/plugin-action-bulk-update": "1.7.0-alpha.10",
- "@nocobase/plugin-action-custom-request": "1.7.0-alpha.10",
- "@nocobase/plugin-action-duplicate": "1.7.0-alpha.10",
- "@nocobase/plugin-action-export": "1.7.0-alpha.10",
- "@nocobase/plugin-action-import": "1.7.0-alpha.10",
- "@nocobase/plugin-action-print": "1.7.0-alpha.10",
- "@nocobase/plugin-ai": "1.7.0-alpha.10",
- "@nocobase/plugin-api-doc": "1.7.0-alpha.10",
- "@nocobase/plugin-api-keys": "1.7.0-alpha.10",
- "@nocobase/plugin-async-task-manager": "1.7.0-alpha.10",
- "@nocobase/plugin-audit-logs": "1.7.0-alpha.10",
- "@nocobase/plugin-auth": "1.7.0-alpha.10",
- "@nocobase/plugin-auth-sms": "1.7.0-alpha.10",
- "@nocobase/plugin-backup-restore": "1.7.0-alpha.10",
- "@nocobase/plugin-block-iframe": "1.7.0-alpha.10",
- "@nocobase/plugin-block-template": "1.7.0-alpha.10",
- "@nocobase/plugin-block-workbench": "1.7.0-alpha.10",
- "@nocobase/plugin-calendar": "1.7.0-alpha.10",
- "@nocobase/plugin-charts": "1.7.0-alpha.10",
- "@nocobase/plugin-client": "1.7.0-alpha.10",
- "@nocobase/plugin-collection-sql": "1.7.0-alpha.10",
- "@nocobase/plugin-collection-tree": "1.7.0-alpha.10",
- "@nocobase/plugin-data-source-main": "1.7.0-alpha.10",
- "@nocobase/plugin-data-source-manager": "1.7.0-alpha.10",
- "@nocobase/plugin-data-visualization": "1.7.0-alpha.10",
- "@nocobase/plugin-environment-variables": "1.7.0-alpha.10",
- "@nocobase/plugin-error-handler": "1.7.0-alpha.10",
- "@nocobase/plugin-field-china-region": "1.7.0-alpha.10",
- "@nocobase/plugin-field-formula": "1.7.0-alpha.10",
- "@nocobase/plugin-field-m2m-array": "1.7.0-alpha.10",
- "@nocobase/plugin-field-markdown-vditor": "1.7.0-alpha.10",
- "@nocobase/plugin-field-sequence": "1.7.0-alpha.10",
- "@nocobase/plugin-field-sort": "1.7.0-alpha.10",
- "@nocobase/plugin-file-manager": "1.7.0-alpha.10",
- "@nocobase/plugin-gantt": "1.7.0-alpha.10",
- "@nocobase/plugin-graph-collection-manager": "1.7.0-alpha.10",
- "@nocobase/plugin-kanban": "1.7.0-alpha.10",
- "@nocobase/plugin-localization": "1.7.0-alpha.10",
- "@nocobase/plugin-logger": "1.7.0-alpha.10",
- "@nocobase/plugin-map": "1.7.0-alpha.10",
- "@nocobase/plugin-mobile": "1.7.0-alpha.10",
- "@nocobase/plugin-mobile-client": "1.7.0-alpha.10",
- "@nocobase/plugin-mock-collections": "1.7.0-alpha.10",
- "@nocobase/plugin-multi-app-manager": "1.7.0-alpha.10",
- "@nocobase/plugin-multi-app-share-collection": "1.7.0-alpha.10",
- "@nocobase/plugin-notification-email": "1.7.0-alpha.10",
- "@nocobase/plugin-notification-in-app-message": "1.7.0-alpha.10",
- "@nocobase/plugin-notification-manager": "1.7.0-alpha.10",
- "@nocobase/plugin-public-forms": "1.7.0-alpha.10",
- "@nocobase/plugin-snapshot-field": "1.7.0-alpha.10",
- "@nocobase/plugin-system-settings": "1.7.0-alpha.10",
- "@nocobase/plugin-theme-editor": "1.7.0-alpha.10",
- "@nocobase/plugin-ui-schema-storage": "1.7.0-alpha.10",
- "@nocobase/plugin-user-data-sync": "1.7.0-alpha.10",
- "@nocobase/plugin-users": "1.7.0-alpha.10",
- "@nocobase/plugin-verification": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-action-trigger": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-aggregate": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-delay": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-dynamic-calculation": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-loop": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-mailer": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-manual": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-notification": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-parallel": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-request": "1.7.0-alpha.10",
- "@nocobase/plugin-workflow-sql": "1.7.0-alpha.10",
- "@nocobase/server": "1.7.0-alpha.10",
+ "@nocobase/json-template-parser": "1.7.0-alpha.11",
+ "@nocobase/plugin-acl": "1.7.0-alpha.11",
+ "@nocobase/plugin-action-bulk-edit": "1.7.0-alpha.11",
+ "@nocobase/plugin-action-bulk-update": "1.7.0-alpha.11",
+ "@nocobase/plugin-action-custom-request": "1.7.0-alpha.11",
+ "@nocobase/plugin-action-duplicate": "1.7.0-alpha.11",
+ "@nocobase/plugin-action-export": "1.7.0-alpha.11",
+ "@nocobase/plugin-action-import": "1.7.0-alpha.11",
+ "@nocobase/plugin-action-print": "1.7.0-alpha.11",
+ "@nocobase/plugin-ai": "1.7.0-alpha.11",
+ "@nocobase/plugin-api-doc": "1.7.0-alpha.11",
+ "@nocobase/plugin-api-keys": "1.7.0-alpha.11",
+ "@nocobase/plugin-async-task-manager": "1.7.0-alpha.11",
+ "@nocobase/plugin-audit-logs": "1.7.0-alpha.11",
+ "@nocobase/plugin-auth": "1.7.0-alpha.11",
+ "@nocobase/plugin-auth-sms": "1.7.0-alpha.11",
+ "@nocobase/plugin-backup-restore": "1.7.0-alpha.11",
+ "@nocobase/plugin-block-iframe": "1.7.0-alpha.11",
+ "@nocobase/plugin-block-template": "1.7.0-alpha.11",
+ "@nocobase/plugin-block-workbench": "1.7.0-alpha.11",
+ "@nocobase/plugin-calendar": "1.7.0-alpha.11",
+ "@nocobase/plugin-charts": "1.7.0-alpha.11",
+ "@nocobase/plugin-client": "1.7.0-alpha.11",
+ "@nocobase/plugin-collection-sql": "1.7.0-alpha.11",
+ "@nocobase/plugin-collection-tree": "1.7.0-alpha.11",
+ "@nocobase/plugin-data-source-main": "1.7.0-alpha.11",
+ "@nocobase/plugin-data-source-manager": "1.7.0-alpha.11",
+ "@nocobase/plugin-data-visualization": "1.7.0-alpha.11",
+ "@nocobase/plugin-departments": "1.7.0-alpha.11",
+ "@nocobase/plugin-environment-variables": "1.7.0-alpha.11",
+ "@nocobase/plugin-error-handler": "1.7.0-alpha.11",
+ "@nocobase/plugin-field-attachment-url": "1.7.0-alpha.11",
+ "@nocobase/plugin-field-china-region": "1.7.0-alpha.11",
+ "@nocobase/plugin-field-formula": "1.7.0-alpha.11",
+ "@nocobase/plugin-field-m2m-array": "1.7.0-alpha.11",
+ "@nocobase/plugin-field-markdown-vditor": "1.7.0-alpha.11",
+ "@nocobase/plugin-field-sequence": "1.7.0-alpha.11",
+ "@nocobase/plugin-field-sort": "1.7.0-alpha.11",
+ "@nocobase/plugin-file-manager": "1.7.0-alpha.11",
+ "@nocobase/plugin-gantt": "1.7.0-alpha.11",
+ "@nocobase/plugin-graph-collection-manager": "1.7.0-alpha.11",
+ "@nocobase/plugin-kanban": "1.7.0-alpha.11",
+ "@nocobase/plugin-locale-tester": "1.7.0-alpha.11",
+ "@nocobase/plugin-localization": "1.7.0-alpha.11",
+ "@nocobase/plugin-logger": "1.7.0-alpha.11",
+ "@nocobase/plugin-map": "1.7.0-alpha.11",
+ "@nocobase/plugin-mobile": "1.7.0-alpha.11",
+ "@nocobase/plugin-mobile-client": "1.7.0-alpha.11",
+ "@nocobase/plugin-mock-collections": "1.7.0-alpha.11",
+ "@nocobase/plugin-multi-app-manager": "1.7.0-alpha.11",
+ "@nocobase/plugin-multi-app-share-collection": "1.7.0-alpha.11",
+ "@nocobase/plugin-notification-email": "1.7.0-alpha.11",
+ "@nocobase/plugin-notification-in-app-message": "1.7.0-alpha.11",
+ "@nocobase/plugin-notification-manager": "1.7.0-alpha.11",
+ "@nocobase/plugin-public-forms": "1.7.0-alpha.11",
+ "@nocobase/plugin-snapshot-field": "1.7.0-alpha.11",
+ "@nocobase/plugin-system-settings": "1.7.0-alpha.11",
+ "@nocobase/plugin-theme-editor": "1.7.0-alpha.11",
+ "@nocobase/plugin-ui-schema-storage": "1.7.0-alpha.11",
+ "@nocobase/plugin-user-data-sync": "1.7.0-alpha.11",
+ "@nocobase/plugin-users": "1.7.0-alpha.11",
+ "@nocobase/plugin-verification": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-action-trigger": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-aggregate": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-delay": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-dynamic-calculation": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-loop": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-mailer": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-manual": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-notification": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-parallel": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-request": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-response-message": "1.7.0-alpha.11",
+ "@nocobase/plugin-workflow-sql": "1.7.0-alpha.11",
+ "@nocobase/server": "1.7.0-alpha.11",
"cronstrue": "^2.11.0",
"fs-extra": "^11.1.1"
},
@@ -129,9 +133,7 @@
"@nocobase/plugin-workflow-action-trigger",
"@nocobase/plugin-workflow-aggregate",
"@nocobase/plugin-workflow-delay",
- "@nocobase/plugin-workflow-dynamic-calculation",
"@nocobase/plugin-workflow-loop",
- "@nocobase/plugin-workflow-manual",
"@nocobase/plugin-workflow-parallel",
"@nocobase/plugin-workflow-request",
"@nocobase/plugin-workflow-sql",
diff --git a/yarn.lock b/yarn.lock
index 0d25e91579..b397c14c95 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -42,15 +42,25 @@
ini "^1.3.5"
kitx "^2.0.0"
+"@alicloud/credentials@^2.4.2":
+ version "2.4.2"
+ resolved "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.4.2.tgz#fa79602b03ba37a425f64732c8a58163a5bcbd0c"
+ integrity sha512-UbqUYlwOWKNxOemXM545HzQyCaChhyrne9cab4f67EqAkgrTjeMiTA7QK6sHtmcmJYowYQxxXoKPSe5GZstvbA==
+ dependencies:
+ "@alicloud/tea-typescript" "^1.8.0"
+ httpx "^2.3.3"
+ ini "^1.3.5"
+ kitx "^2.0.0"
+
"@alicloud/dingtalk@^2.1.23":
- version "2.1.91"
- resolved "https://registry.npmmirror.com/@alicloud/dingtalk/-/dingtalk-2.1.91.tgz#9c2aaf753c6b6c382506a45c52d296ba4f92a1e5"
- integrity sha512-mmxz7JaEZRmGx2DKPrtNc4utWfy2z31HNxK8afezEnvKDybE77cpE/cPO9jS6f+sd6j9Sb0Obo370BjKLV0P1g==
+ version "2.1.98"
+ resolved "https://registry.npmmirror.com/@alicloud/dingtalk/-/dingtalk-2.1.98.tgz#2150efcd516191b0b152d298d004d970c4f72b89"
+ integrity sha512-qKlXk8D1E0506uLL2F3grTq/S2LmceRhVTM/WlGZLFtMMC/72QfYkQ77jqC/M8vDzj7POAXLE/7f5hwl3QuNwQ==
dependencies:
"@alicloud/endpoint-util" "^0.0.1"
"@alicloud/gateway-dingtalk" "^1.0.2"
"@alicloud/gateway-spi" "^0.0.8"
- "@alicloud/openapi-client" "^0.4.12"
+ "@alicloud/openapi-client" "^0.4.14"
"@alicloud/openapi-util" "^0.3.2"
"@alicloud/tea-typescript" "^1.7.1"
"@alicloud/tea-util" "^1.4.9"
@@ -91,7 +101,7 @@
"@alicloud/credentials" "^2"
"@alicloud/tea-typescript" "^1.7.1"
-"@alicloud/openapi-client@0.4.12", "@alicloud/openapi-client@^0.4.1", "@alicloud/openapi-client@^0.4.12", "@alicloud/openapi-client@^0.4.7", "@alicloud/openapi-client@^0.4.8", "@alicloud/openapi-client@^0.4.9":
+"@alicloud/openapi-client@0.4.12", "@alicloud/openapi-client@^0.4.1":
version "0.4.12"
resolved "https://registry.npmmirror.com/@alicloud/openapi-client/-/openapi-client-0.4.12.tgz#7a4f00e71b025d8a487cfbeeb8aa6f63d70b2054"
integrity sha512-WuKfFqwY3/+wuNawzfJAirNA00XDI7fm9fUhWK7siGZEh0R2XJR0Y54MLW7WWItX06fAghUvnDhKWZH3AgN+yg==
@@ -103,6 +113,18 @@
"@alicloud/tea-util" "^1.4.9"
"@alicloud/tea-xml" "0.0.3"
+"@alicloud/openapi-client@^0.4.14", "@alicloud/openapi-client@^0.4.7", "@alicloud/openapi-client@^0.4.8", "@alicloud/openapi-client@^0.4.9":
+ version "0.4.14"
+ resolved "https://registry.npmmirror.com/@alicloud/openapi-client/-/openapi-client-0.4.14.tgz#3f408a8e7e6ad7e1026ff96304fadaf9e8976e87"
+ integrity sha512-NiMDBszCyiH5HI9vHbkDhhDbFF3gMEJDHuPc2cAP0queLtrjPfU+d6/uhGVt44B9oC0q6f6vaJgptQ99fxxfnQ==
+ dependencies:
+ "@alicloud/credentials" "^2.4.2"
+ "@alicloud/gateway-spi" "^0.0.8"
+ "@alicloud/openapi-util" "^0.3.2"
+ "@alicloud/tea-typescript" "^1.7.1"
+ "@alicloud/tea-util" "1.4.9"
+ "@alicloud/tea-xml" "0.0.3"
+
"@alicloud/openapi-util@^0.2.9":
version "0.2.9"
resolved "https://registry.npmmirror.com/@alicloud/openapi-util/-/openapi-util-0.2.9.tgz#2379cd81f993dcab32066a2b892ddcbdd266d51c"
@@ -139,7 +161,7 @@
"@alicloud/tea-typescript" "^1.5.1"
kitx "^2.0.0"
-"@alicloud/tea-util@^1.3.0", "@alicloud/tea-util@^1.4.4", "@alicloud/tea-util@^1.4.9":
+"@alicloud/tea-util@1.4.9", "@alicloud/tea-util@^1.3.0", "@alicloud/tea-util@^1.4.4", "@alicloud/tea-util@^1.4.9":
version "1.4.9"
resolved "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.9.tgz#aa1c4f566d530e7ffc6d9fac1aded06bb0ea45ca"
integrity sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw==
@@ -1496,9 +1518,9 @@
tslib "^2.6.2"
"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.9.2":
- version "1.9.2"
- resolved "https://registry.npmmirror.com/@azure/core-client/-/core-client-1.9.2.tgz#6fc69cee2816883ab6c5cdd653ee4f2ff9774f74"
- integrity sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==
+ version "1.9.3"
+ resolved "https://registry.npmmirror.com/@azure/core-client/-/core-client-1.9.3.tgz#9ca8f3bdc730d10d58f65c9c2c9ca992bc15bb67"
+ integrity sha512-/wGw8fJ4mdpJ1Cum7s1S+VQyXt1ihwKLzfabS1O/RDADnmzVc01dHn44qD0BvGH6KlZNzOMW95tEpKqhkCChPA==
dependencies:
"@azure/abort-controller" "^2.0.0"
"@azure/core-auth" "^1.4.0"
@@ -1535,9 +1557,9 @@
tslib "^2.6.2"
"@azure/core-rest-pipeline@^1.17.0", "@azure/core-rest-pipeline@^1.19.0", "@azure/core-rest-pipeline@^1.8.0", "@azure/core-rest-pipeline@^1.8.1", "@azure/core-rest-pipeline@^1.9.1":
- version "1.19.0"
- resolved "https://registry.npmmirror.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.0.tgz#4cc60d3f2ee68cf0ef379851b4ed175f7932c8c5"
- integrity sha512-bM3308LRyg5g7r3Twprtqww0R/r7+GyVxj4BafcmVPo4WQoGt5JXuaqxHEFjw2o3rvFZcUPiqJMg6WuvEEeVUA==
+ version "1.19.1"
+ resolved "https://registry.npmmirror.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.1.tgz#e740676444777a04dc55656d8660131dfd926924"
+ integrity sha512-zHeoI3NCs53lLBbWNzQycjnYKsA1CVKlnzSNuSFcUDwBp8HHVObePxrM7HaX+Ha5Ks639H7chNC9HOaIhNS03w==
dependencies:
"@azure/abort-controller" "^2.0.0"
"@azure/core-auth" "^1.8.0"
@@ -1564,9 +1586,9 @@
tslib "^2.6.2"
"@azure/identity@^4.2.1":
- version "4.7.0"
- resolved "https://registry.npmmirror.com/@azure/identity/-/identity-4.7.0.tgz#b3bc57aec40432899108fd41177e8168e7bb6223"
- integrity sha512-6z/S2KorkbKaZ0DgZFVRdu7RCuATmMSTjKpuhj7YpjxkJ0vnJ7kTM3cpNgzFgk9OPYfZ31wrBEtC/iwAS4jQDA==
+ version "4.9.0"
+ resolved "https://registry.npmmirror.com/@azure/identity/-/identity-4.9.0.tgz#226c21bcb706401d1e2425834158e448f8d01209"
+ integrity sha512-dz2ZvKxDFoTUmJgmkCBVcyuKckgqE1qVxrRPzUhyKN7FyvUbtNPUrGzqSllOAf1OL9TMGgYqZWbIyD0b/AE15g==
dependencies:
"@azure/abort-controller" "^2.0.0"
"@azure/core-auth" "^1.9.0"
@@ -1576,11 +1598,8 @@
"@azure/core-util" "^1.11.0"
"@azure/logger" "^1.0.0"
"@azure/msal-browser" "^4.2.0"
- "@azure/msal-node" "^3.2.1"
- events "^3.0.0"
- jws "^4.0.0"
+ "@azure/msal-node" "^3.5.0"
open "^10.1.0"
- stoppable "^1.1.0"
tslib "^2.2.0"
"@azure/keyvault-common@^2.0.0":
@@ -1623,21 +1642,21 @@
tslib "^2.6.2"
"@azure/msal-browser@^4.2.0":
- version "4.5.0"
- resolved "https://registry.npmmirror.com/@azure/msal-browser/-/msal-browser-4.5.0.tgz#fbdc4f58f0f37a5487199f4706e5f8a04cd00234"
- integrity sha512-H7mWmu8yI0n0XxhJobrgncXI6IU5h8DKMiWDHL5y+Dc58cdg26GbmaMUehbUkdKAQV2OTiFa4FUa6Fdu/wIxBg==
+ version "4.11.0"
+ resolved "https://registry.npmmirror.com/@azure/msal-browser/-/msal-browser-4.11.0.tgz#e9d9651d692969e68c78ef873ed9a69e02389a64"
+ integrity sha512-0p5Ut3wORMP+975AKvaSPIO4UytgsfAvJ7RxaTx+nkP+Hpkmm93AuiMkBWKI2x9tApU/SLgIyPz/ZwLYUIWb5Q==
dependencies:
- "@azure/msal-common" "15.2.0"
+ "@azure/msal-common" "15.5.1"
"@azure/msal-common@14.16.0":
version "14.16.0"
resolved "https://registry.npmmirror.com/@azure/msal-common/-/msal-common-14.16.0.tgz#f3470fcaec788dbe50859952cd499340bda23d7a"
integrity sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==
-"@azure/msal-common@15.2.0":
- version "15.2.0"
- resolved "https://registry.npmmirror.com/@azure/msal-common/-/msal-common-15.2.0.tgz#f4e38ba85c0a32208b7046e011c21ff627b6755c"
- integrity sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==
+"@azure/msal-common@15.5.1":
+ version "15.5.1"
+ resolved "https://registry.npmmirror.com/@azure/msal-common/-/msal-common-15.5.1.tgz#3b34c81013530e1425a1fad40f3ac1238e1780f8"
+ integrity sha512-oxK0khbc4Bg1bKQnqDr7ikULhVL2OHgSrIq0Vlh4b6+hm4r0lr6zPMQE8ZvmacJuh+ZZGKBM5iIObhF1q1QimQ==
"@azure/msal-node@^2.15.0":
version "2.16.2"
@@ -1648,12 +1667,12 @@
jsonwebtoken "^9.0.0"
uuid "^8.3.0"
-"@azure/msal-node@^3.2.1":
- version "3.2.3"
- resolved "https://registry.npmmirror.com/@azure/msal-node/-/msal-node-3.2.3.tgz#c742d66d3a9183c1dfa4160632d8891c69af7bcc"
- integrity sha512-0eaPqBIWEAizeYiXdeHb09Iq0tvHJ17ztvNEaLdr/KcJJhJxbpkkEQf09DB+vKlFE0tzYi7j4rYLTXtES/InEQ==
+"@azure/msal-node@^3.5.0":
+ version "3.5.1"
+ resolved "https://registry.npmmirror.com/@azure/msal-node/-/msal-node-3.5.1.tgz#8bb233cbeeda83f64af4cc29569f1b5312c9b9ad"
+ integrity sha512-dkgMYM5B6tI88r/oqf5bYd93WkenQpaWwiszJDk7avVjso8cmuKRTW97dA1RMi6RhihZFLtY1VtWxU9+sW2T5g==
dependencies:
- "@azure/msal-common" "15.2.0"
+ "@azure/msal-common" "15.5.1"
jsonwebtoken "^9.0.0"
uuid "^8.3.0"
@@ -2993,9 +3012,9 @@
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime-corejs3@^7.15.4":
- version "7.26.9"
- resolved "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.26.9.tgz#8b73bae47453aa3dd2839ac52598581a7dd8332f"
- integrity sha512-5EVjbTegqN7RSJle6hMWYxO4voo4rI+9krITk+DWR+diJgGrjZjrIBnJhjrHYYQsFgI7j1w1QnrvV7YSKBfYGg==
+ version "7.27.0"
+ resolved "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz#c766df350ec7a2caf3ed64e3659b100954589413"
+ integrity sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==
dependencies:
core-js-pure "^3.30.2"
regenerator-runtime "^0.14.0"
@@ -3194,7 +3213,7 @@
resolved "https://registry.npmmirror.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6"
integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==
-"@codemirror/autocomplete@^6.0.0":
+"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.3.2":
version "6.18.6"
resolved "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz#de26e864a1ec8192a1b241eb86addbb612964ddb"
integrity sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==
@@ -3204,17 +3223,25 @@
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
-"@codemirror/commands@^6.0.0":
- version "6.8.0"
- resolved "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.8.0.tgz#92f200b66f852939bd6ebb90d48c2d9e9c813d64"
- integrity sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==
+"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0":
+ version "6.8.1"
+ resolved "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.8.1.tgz#639f5559d2f33f2582a2429c58cb0c1b925c7a30"
+ integrity sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.4.0"
"@codemirror/view" "^6.27.0"
"@lezer/common" "^1.1.0"
-"@codemirror/lang-javascript@^6.2.2":
+"@codemirror/lang-java@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.npmmirror.com/@codemirror/lang-java/-/lang-java-6.0.1.tgz#03bd06334da7c8feb9dff6db01ac6d85bd2e48bb"
+ integrity sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==
+ dependencies:
+ "@codemirror/language" "^6.0.0"
+ "@lezer/java" "^1.0.0"
+
+"@codemirror/lang-javascript@^6.2.2", "@codemirror/lang-javascript@^6.2.3":
version "6.2.3"
resolved "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz#d705c359dc816afcd3bcdf120a559f83d31d4cda"
integrity sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==
@@ -3227,10 +3254,33 @@
"@lezer/common" "^1.0.0"
"@lezer/javascript" "^1.0.0"
-"@codemirror/language@^6.0.0", "@codemirror/language@^6.6.0":
- version "6.10.8"
- resolved "https://registry.npmmirror.com/@codemirror/language/-/language-6.10.8.tgz#3e3a346a2b0a8cf63ee1cfe03349eb1965dce5f9"
- integrity sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==
+"@codemirror/lang-python@^6.1.7":
+ version "6.1.7"
+ resolved "https://registry.npmmirror.com/@codemirror/lang-python/-/lang-python-6.1.7.tgz#1906234d453517d760a57c69672f0023a8649e14"
+ integrity sha512-mZnFTsL4lW5p9ch8uKNKeRU3xGGxr1QpESLilfON2E3fQzOa/OygEMkaDvERvXDJWJA9U9oN/D4w0ZuUzNO4+g==
+ dependencies:
+ "@codemirror/autocomplete" "^6.3.2"
+ "@codemirror/language" "^6.8.0"
+ "@codemirror/state" "^6.0.0"
+ "@lezer/common" "^1.2.1"
+ "@lezer/python" "^1.1.4"
+
+"@codemirror/lang-sql@^6.8.0":
+ version "6.8.0"
+ resolved "https://registry.npmmirror.com/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz#1ae68ad49f378605ff88a4cc428ba667ce056068"
+ integrity sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==
+ dependencies:
+ "@codemirror/autocomplete" "^6.0.0"
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@lezer/common" "^1.2.0"
+ "@lezer/highlight" "^1.0.0"
+ "@lezer/lr" "^1.0.0"
+
+"@codemirror/language@^6.0.0", "@codemirror/language@^6.11.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0":
+ version "6.11.0"
+ resolved "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.0.tgz#5ae90972601497f4575f30811519d720bf7232c9"
+ integrity sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.23.0"
@@ -3239,10 +3289,17 @@
"@lezer/lr" "^1.0.0"
style-mod "^4.0.0"
+"@codemirror/legacy-modes@^6.5.0":
+ version "6.5.0"
+ resolved "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.0.tgz#21c8cf818f9ea4d6eba9f22afdfef010d1d9f754"
+ integrity sha512-dNw5pwTqtR1giYjaJyEajunLqxGavZqV0XRtVZyMJnNOD2HmK9DMUmuCAr6RMFGRJ4l8OeQDjpI/us+R09mQsw==
+ dependencies:
+ "@codemirror/language" "^6.0.0"
+
"@codemirror/lint@^6.0.0", "@codemirror/lint@^6.8.2":
- version "6.8.4"
- resolved "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.4.tgz#7d8aa5d1a6dec89ffcc23ad45ddca2e12e90982d"
- integrity sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==
+ version "6.8.5"
+ resolved "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.5.tgz#9edaa808e764e28e07665b015951934c8ec3a418"
+ integrity sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.35.0"
@@ -3257,17 +3314,27 @@
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
-"@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.4.1", "@codemirror/state@^6.5.0":
+"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.4.0", "@codemirror/state@^6.4.1", "@codemirror/state@^6.5.0":
version "6.5.2"
resolved "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz#8eca3a64212a83367dc85475b7d78d5c9b7076c6"
integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==
dependencies:
"@marijn/find-cluster-break" "^1.0.0"
+"@codemirror/theme-one-dark@^6.0.0":
+ version "6.1.2"
+ resolved "https://registry.npmmirror.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8"
+ integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==
+ dependencies:
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ "@lezer/highlight" "^1.0.0"
+
"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.32.0", "@codemirror/view@^6.35.0":
- version "6.36.4"
- resolved "https://registry.npmmirror.com/@codemirror/view/-/view-6.36.4.tgz#d47d38b92a22cc40647bfb9cc97944e13d44942d"
- integrity sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==
+ version "6.36.5"
+ resolved "https://registry.npmmirror.com/@codemirror/view/-/view-6.36.5.tgz#bb99b971322b9a3f8c7013f0ef6c4a511c0d750a"
+ integrity sha512-cd+FZEUlu3GQCYnguYm3EkhJ8KJVisqqUsCOKedBoAt/d9c76JUUap6U0UrpElln5k6VyrEOYliMuDAKIeDQLg==
dependencies:
"@codemirror/state" "^6.5.0"
style-mod "^4.1.0"
@@ -5026,6 +5093,13 @@
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
+"@isaacs/fs-minipass@^4.0.0":
+ version "4.0.1"
+ resolved "https://registry.npmmirror.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32"
+ integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==
+ dependencies:
+ minipass "^7.0.4"
+
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.npmmirror.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -5173,9 +5247,9 @@
"@jridgewell/sourcemap-codec" "^1.4.14"
"@js-joda/core@^5.6.1":
- version "5.6.4"
- resolved "https://registry.npmmirror.com/@js-joda/core/-/core-5.6.4.tgz#abafce9b30302639bfe4a71bad5120fe39654f10"
- integrity sha512-ChdLDTYMEoYoiKZMT90wZMEdGvZ2/QZMnhvjvEqeO5oLoxUfSiLzfe6Lhf3g88+MhZ+utbAu7PAxX1sZkLo5pA==
+ version "5.6.5"
+ resolved "https://registry.npmmirror.com/@js-joda/core/-/core-5.6.5.tgz#c766894b49eb8044480b91625fb7dc370e8182ef"
+ integrity sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==
"@jsep-plugin/assignment@^1.2.1", "@jsep-plugin/assignment@^1.3.0":
version "1.3.0"
@@ -6016,7 +6090,7 @@
npmlog "^4.1.2"
write-file-atomic "^3.0.3"
-"@lezer/common@^1.0.0", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0":
+"@lezer/common@^1.0.0", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0", "@lezer/common@^1.2.1":
version "1.2.3"
resolved "https://registry.npmmirror.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd"
integrity sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==
@@ -6028,10 +6102,19 @@
dependencies:
"@lezer/common" "^1.0.0"
+"@lezer/java@^1.0.0":
+ version "1.1.3"
+ resolved "https://registry.npmmirror.com/@lezer/java/-/java-1.1.3.tgz#9efd6a29b4142d07f211076a6fb5e8061c85e147"
+ integrity sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==
+ dependencies:
+ "@lezer/common" "^1.2.0"
+ "@lezer/highlight" "^1.0.0"
+ "@lezer/lr" "^1.0.0"
+
"@lezer/javascript@^1.0.0":
- version "1.4.21"
- resolved "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.4.21.tgz#8ebf7d1f891c70e3d00864f5a03ac42c75d19492"
- integrity sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==
+ version "1.5.0"
+ resolved "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.0.tgz#f332792c75af16809e09b6ed50cc7b69d25bd5e9"
+ integrity sha512-i/uZt1eoiojC3BRsjtiYZjT8DhzgZvWiKJjpXW3Jc2y4FkXY9YoBLKx+jSct+ynUINv5GtRxjTK7hBNhujPwrg==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.1.3"
@@ -6044,6 +6127,15 @@
dependencies:
"@lezer/common" "^1.0.0"
+"@lezer/python@^1.1.4":
+ version "1.1.18"
+ resolved "https://registry.npmmirror.com/@lezer/python/-/python-1.1.18.tgz#fa02fbf492741c82dc2dc98a0a042bd0d4d7f1d3"
+ integrity sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==
+ dependencies:
+ "@lezer/common" "^1.2.0"
+ "@lezer/highlight" "^1.0.0"
+ "@lezer/lr" "^1.0.0"
+
"@ljharb/resumer@~0.0.1":
version "0.0.1"
resolved "https://registry.npmmirror.com/@ljharb/resumer/-/resumer-0.0.1.tgz#8a940a9192dd31f6a1df17564bbd26dc6ad3e68d"
@@ -6105,33 +6197,48 @@
resolved "https://registry.npmmirror.com/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.40.0.tgz#65f51600ab45ace97d7b1368c47f9e0f835fddca"
integrity sha512-1fcPVrB/NkbNcGNfCy+Cgnvwxt6/sbIEEFgZHFBJ670zYLegENYJF8qMo7x3LqBjWX2/Eneq5BVVRCLTmlJN+g==
-"@module-federation/runtime-tools@0.5.1":
- version "0.5.1"
- resolved "https://registry.npmmirror.com/@module-federation/runtime-tools/-/runtime-tools-0.5.1.tgz#1b1f93837159a6bf0c0ba78730d589a5a8f74aa3"
- integrity sha512-nfBedkoZ3/SWyO0hnmaxuz0R0iGPSikHZOAZ0N/dVSQaIzlffUo35B5nlC2wgWIc0JdMZfkwkjZRrnuuDIJbzg==
- dependencies:
- "@module-federation/runtime" "0.5.1"
- "@module-federation/webpack-bundler-runtime" "0.5.1"
+"@module-federation/error-codes@0.11.2":
+ version "0.11.2"
+ resolved "https://registry.npmmirror.com/@module-federation/error-codes/-/error-codes-0.11.2.tgz#880cbaf370bacb5d27e5149a93228aebe7ed084c"
+ integrity sha512-ik1Qnn0I+WyEdprTck9WGlH41vGsVdUg8cfO+ZM02qOb2cZm5Vu3SlxGAobj6g7uAj0g8yINnd7h7Dci40BxQA==
-"@module-federation/runtime@0.5.1":
- version "0.5.1"
- resolved "https://registry.npmmirror.com/@module-federation/runtime/-/runtime-0.5.1.tgz#b548a75e2068952ff66ad717cbf73fc921edd5d7"
- integrity sha512-xgiMUWwGLWDrvZc9JibuEbXIbhXg6z2oUkemogSvQ4LKvrl/n0kbqP1Blk669mXzyWbqtSp6PpvNdwaE1aN5xQ==
+"@module-federation/runtime-core@0.11.2":
+ version "0.11.2"
+ resolved "https://registry.npmmirror.com/@module-federation/runtime-core/-/runtime-core-0.11.2.tgz#677aced902d56afd3e44f4033e8d78d57c8aa029"
+ integrity sha512-dia5kKybi6MFU0s5PgglJwN27k7n9Sf69Cy5xZ4BWaP0qlaXTsxHKO0PECHNt2Pt8jDdyU29sQ4DwAQfxpnXJQ==
dependencies:
- "@module-federation/sdk" "0.5.1"
+ "@module-federation/error-codes" "0.11.2"
+ "@module-federation/sdk" "0.11.2"
-"@module-federation/sdk@0.5.1":
- version "0.5.1"
- resolved "https://registry.npmmirror.com/@module-federation/sdk/-/sdk-0.5.1.tgz#6c0a4053c23fa84db7aae7e4736496c541de7191"
- integrity sha512-exvchtjNURJJkpqjQ3/opdbfeT2wPKvrbnGnyRkrwW5o3FH1LaST1tkiNviT6OXTexGaVc2DahbdniQHVtQ7pA==
-
-"@module-federation/webpack-bundler-runtime@0.5.1":
- version "0.5.1"
- resolved "https://registry.npmmirror.com/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.5.1.tgz#ef626af0d57e3568c474d66d7d3797366e09cafd"
- integrity sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA==
+"@module-federation/runtime-tools@0.11.2":
+ version "0.11.2"
+ resolved "https://registry.npmmirror.com/@module-federation/runtime-tools/-/runtime-tools-0.11.2.tgz#d6a5c4f61b93b647de656b08ba465590631a1316"
+ integrity sha512-4MJTGAxVq6vxQRkTtTlH7Mm9AVqgn0X9kdu+7RsL7T/qU+jeYsbrntN2CWG3GVVA8r5JddXyTI1iJ0VXQZLV1w==
dependencies:
- "@module-federation/runtime" "0.5.1"
- "@module-federation/sdk" "0.5.1"
+ "@module-federation/runtime" "0.11.2"
+ "@module-federation/webpack-bundler-runtime" "0.11.2"
+
+"@module-federation/runtime@0.11.2":
+ version "0.11.2"
+ resolved "https://registry.npmmirror.com/@module-federation/runtime/-/runtime-0.11.2.tgz#e623136774599ce202bb4ea1e396f18fbaeee19a"
+ integrity sha512-Ya9u/L6z2LvhgpqxuKCB7LcigIIRf1BbaxAZIH7mzbq/A7rZtTP7v+73E433jvgiAlbAfPSZkeoYGele6hfRwA==
+ dependencies:
+ "@module-federation/error-codes" "0.11.2"
+ "@module-federation/runtime-core" "0.11.2"
+ "@module-federation/sdk" "0.11.2"
+
+"@module-federation/sdk@0.11.2":
+ version "0.11.2"
+ resolved "https://registry.npmmirror.com/@module-federation/sdk/-/sdk-0.11.2.tgz#965b0dcf8fb036dda9b1e6812d6ae0a394ea827d"
+ integrity sha512-SBFe5xOamluT900J4AGBx+2/kCH/JbfqXoUwPSAC6PRzb8Y7LB0posnOGzmqYsLZXT37vp3d6AmJDsVoajDqxw==
+
+"@module-federation/webpack-bundler-runtime@0.11.2":
+ version "0.11.2"
+ resolved "https://registry.npmmirror.com/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.11.2.tgz#ef4a21e0ff8aefce9c264a57aa882ee72ecfe6aa"
+ integrity sha512-WdwIE6QF+MKs/PdVu0cKPETF743JB9PZ62/qf7Uo3gU4fjsUMc37RnbJZ/qB60EaHHfjwp1v6NnhZw1r4eVsnw==
+ dependencies:
+ "@module-federation/runtime" "0.11.2"
+ "@module-federation/sdk" "0.11.2"
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
@@ -6367,36 +6474,6 @@
dependencies:
"@octokit/openapi-types" "^12.11.0"
-"@opencensus/core@0.0.9":
- version "0.0.9"
- resolved "https://registry.npmmirror.com/@opencensus/core/-/core-0.0.9.tgz#b16f775435ee309433e4126af194d37313fc93b3"
- integrity sha512-31Q4VWtbzXpVUd2m9JS6HEaPjlKvNMOiF7lWKNmXF84yUcgfAFL5re7/hjDmdyQbOp32oGc+RFV78jXIldVz6Q==
- dependencies:
- continuation-local-storage "^3.2.1"
- log-driver "^1.2.7"
- semver "^5.5.0"
- shimmer "^1.2.0"
- uuid "^3.2.1"
-
-"@opencensus/core@^0.0.8":
- version "0.0.8"
- resolved "https://registry.npmmirror.com/@opencensus/core/-/core-0.0.8.tgz#df01f200c2d2fbfe14dae129a1a86fb87286db92"
- integrity sha512-yUFT59SFhGMYQgX0PhoTR0LBff2BEhPrD9io1jWfF/VDbakRfs6Pq60rjv0Z7iaTav5gQlttJCX2+VPxFWCuoQ==
- dependencies:
- continuation-local-storage "^3.2.1"
- log-driver "^1.2.7"
- semver "^5.5.0"
- shimmer "^1.2.0"
- uuid "^3.2.1"
-
-"@opencensus/propagation-b3@0.0.8":
- version "0.0.8"
- resolved "https://registry.npmmirror.com/@opencensus/propagation-b3/-/propagation-b3-0.0.8.tgz#0751e6fd75f09400d9d3c419001e9e15a0df68e9"
- integrity sha512-PffXX2AL8Sh0VHQ52jJC4u3T0H6wDK6N/4bg7xh4ngMYOIi13aR1kzVvX1sVDBgfGwDOkMbl4c54Xm3tlPx/+A==
- dependencies:
- "@opencensus/core" "^0.0.8"
- uuid "^3.2.1"
-
"@opentelemetry/api@^1.7.0":
version "1.7.0"
resolved "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.7.0.tgz#b139c81999c23e3c8d3c0a7234480e945920fc40"
@@ -6563,32 +6640,29 @@
dependencies:
playwright "1.45.3"
-"@pm2/agent@~2.0.0":
- version "2.0.3"
- resolved "https://registry.npmmirror.com/@pm2/agent/-/agent-2.0.3.tgz#6b47fda837f185864767fe1e048f61d1de31fc45"
- integrity sha512-xkqqCoTf5VsciMqN0vb9jthW7olVAi4KRFNddCc7ZkeJZ3i8QwZANr4NSH2H5DvseRFHq7MiPspRY/EWAFWWTg==
+"@pm2/agent@~2.1.1":
+ version "2.1.1"
+ resolved "https://registry.npmmirror.com/@pm2/agent/-/agent-2.1.1.tgz#b74dc0cc97e59827307fd6b9a4ebb5aeb40473fb"
+ integrity sha512-0V9ckHWd/HSC8BgAbZSoq8KXUG81X97nSkAxmhKDhmF8vanyaoc1YXwc2KVkbWz82Rg4gjd2n9qiT3i7bdvGrQ==
dependencies:
async "~3.2.0"
chalk "~3.0.0"
dayjs "~1.8.24"
debug "~4.3.1"
eventemitter2 "~5.0.1"
- fast-json-patch "^3.0.0-1"
+ fast-json-patch "^3.1.0"
fclone "~1.0.11"
- nssocket "0.6.0"
pm2-axon "~4.0.1"
pm2-axon-rpc "~0.7.0"
- proxy-agent "~6.3.0"
+ proxy-agent "~6.4.0"
semver "~7.5.0"
- ws "~7.4.0"
+ ws "~7.5.10"
-"@pm2/io@~5.0.0":
- version "5.0.2"
- resolved "https://registry.npmmirror.com/@pm2/io/-/io-5.0.2.tgz#5e4177281280082d7c490bb776fad7f8448c6bca"
- integrity sha512-XAvrNoQPKOyO/jJyCu8jPhLzlyp35MEf7w/carHXmWKddPzeNOFSEpSEqMzPDawsvpxbE+i918cNN+MwgVsStA==
+"@pm2/io@~6.1.0":
+ version "6.1.0"
+ resolved "https://registry.npmmirror.com/@pm2/io/-/io-6.1.0.tgz#37ea7908e2a1dd7b88862f60d98d8b1f495ee16a"
+ integrity sha512-IxHuYURa3+FQ6BKePlgChZkqABUKFYH6Bwbw7V/pWU1pP6iR1sCI26l7P9ThUEB385ruZn/tZS3CXDUF5IA1NQ==
dependencies:
- "@opencensus/core" "0.0.9"
- "@opencensus/propagation-b3" "0.0.8"
async "~2.6.1"
debug "~4.3.1"
eventemitter2 "^6.3.1"
@@ -6598,15 +6672,15 @@
signal-exit "^3.0.3"
tslib "1.9.3"
-"@pm2/js-api@~0.6.7":
- version "0.6.7"
- resolved "https://registry.npmmirror.com/@pm2/js-api/-/js-api-0.6.7.tgz#ed28c3b7b6d26f03f826318754fdc5468afa589f"
- integrity sha512-jiJUhbdsK+5C4zhPZNnyA3wRI01dEc6a2GhcQ9qI38DyIk+S+C8iC3fGjcjUbt/viLYKPjlAaE+hcT2/JMQPXw==
+"@pm2/js-api@~0.8.0":
+ version "0.8.0"
+ resolved "https://registry.npmmirror.com/@pm2/js-api/-/js-api-0.8.0.tgz#d1b8aff562dd34befa3cb30fe28e08c9f9743abc"
+ integrity sha512-nmWzrA/BQZik3VBz+npRcNIu01kdBhWL0mxKmP1ciF/gTcujPTQqt027N9fc1pK9ERM8RipFhymw7RcmCyOEYA==
dependencies:
async "^2.6.3"
- axios "^0.21.0"
debug "~4.3.1"
eventemitter2 "^6.3.1"
+ extrareqp2 "^1.0.0"
ws "^7.0.0"
"@pm2/pm2-version-check@latest":
@@ -7087,75 +7161,76 @@
rslog "^1.2.3"
strip-ansi "^6.0.1"
-"@rspack/binding-darwin-arm64@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.1.1.tgz#e03df97bebab2ef6ccbbef940c8ef092b37c8336"
- integrity sha512-BnvGPWObGZ2ZVnxe4K3NKwAWxYubOJvfwporXWD3NgkzeV5xJqGBFWRDnr/nfsFpgCTI8goxK5db/wb7NVzLqg==
+"@rspack/binding-darwin-arm64@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.3.2.tgz#7d7d9db21489d4afb0c5f05d9af4b879f4f2751f"
+ integrity sha512-oeZvdHCY3XML8U6npof3b7uNVmNMTIRccPe2IDHlV1zk1MPfBzgrKOKmo1V8kqI43xAWET7CpAX9C+TjDDcy/g==
-"@rspack/binding-darwin-x64@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.1.1.tgz#ce3893eee19e4f43b27e56b0fd2737b97efd69c0"
- integrity sha512-aiwJRkPGAg99vCrG/C9I87Fh9TShOAkzpf2yeJEZL4gwTj9A8wrc/xlrCFn1BDkbPnGYz62oCR7z6JLIDgYLuA==
+"@rspack/binding-darwin-x64@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.3.2.tgz#e9b910b70911c6f5e16099b7ba7c0e65ee119497"
+ integrity sha512-V1IKH3I0uEf4vjou158amWgpAUz9MgGiFU09LgZS/hz1jYMTCi3Z791EEL4Gz6iqAixIZxtw6aYeotjRJ4Kyqg==
-"@rspack/binding-linux-arm64-gnu@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.1.tgz#b9ba4d0cfc39fec5c2db9d3f75327c3b8a383e96"
- integrity sha512-2Z8YxH4+V0MiNhVQ2IFELDIFtykIdKgmOmGr/PuRQMHMxSn8AKo5uqBD30sZqe0+gryplZwK3hyrBETHOmSltQ==
+"@rspack/binding-linux-arm64-gnu@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.3.2.tgz#b7e540c1b95cb7547aa2d346ef113db30bc75988"
+ integrity sha512-nJzY+Ur6FxWM0xc+G2tY1TQu3s6qgolxXb5K2VLIDHSPqDAjqRc35ypQc9Tz3rUPb8HVh+X7YLIZxA0hE4eQOg==
-"@rspack/binding-linux-arm64-musl@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.1.tgz#bb98d98d703c6a0c69489975821a3d3236fa91cf"
- integrity sha512-l+cJd3wAxBt523Min7qN+G5s3SU0rif9Yq2AFWWl+R6IvmnMlMq6sAAyiyogUidFmJ5XIKSJJBTBnvLF3g4ezg==
+"@rspack/binding-linux-arm64-musl@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.3.2.tgz#29f7bc0461a4ec91b3cd86b21393360c73894b19"
+ integrity sha512-sRi77ccO/oOfyBNq3FgW2pDtXcgMzslLokOby8NpD/kv/SxtOE4ORoLZKzdJyGNh2WDPbtSwIDWPes2x4MKASQ==
-"@rspack/binding-linux-x64-gnu@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.1.tgz#17d7ceac270ffc5980a16e31badef68136b31d51"
- integrity sha512-goaDDrXNulR7FcvUfj8AjhF3g7IXUttjQ4QsfY2xz7s20tDETlq5HpcM2A8GEI6lqkPAv/ITU0AynLK7bfyr4A==
+"@rspack/binding-linux-x64-gnu@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.3.2.tgz#8771bfa8d18198e8b776d7e0ee33b5d29f0ccabe"
+ integrity sha512-KnrFQUj6SKJFGXqJW9Kgdv+mRGcPCirQesuwXtW+9YejT6MzLRRdJ4NDQdfcmfLZK9+ap+l73bLXAyMiIBZiOw==
-"@rspack/binding-linux-x64-musl@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.1.tgz#bfc6363ae73ffd04e7af00f134d640ec0407a730"
- integrity sha512-T4RRn9ycxUHAfZJpfNRy+DdfevTXIZqox+NNg/N3d+Pqj5QS3zqpHBfPLC2mIIN1dw55BoshRIP2C1hUG0Fk6g==
+"@rspack/binding-linux-x64-musl@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.3.2.tgz#7ea645db3bef68f162a06c16cfa0563576bb51ef"
+ integrity sha512-ZcTl4LBgxp5Bfyu9x7NhYRAR4qWPwhhxzwXmiQ1ya7DsdqiYaiCr59dPQx7ZaExXckeHGly75B3aTn1II9Vexw==
-"@rspack/binding-win32-arm64-msvc@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.1.tgz#efbd8c90d0097907104da2f5d75416655cfb0f60"
- integrity sha512-FHIPpueFc/+vWdZeVWRYWW0Z0IsDIHy+WhWxITeLjOVGsUN4rhaztYOausD7WsOlOhmR0SddeOYtRs/BR35wig==
+"@rspack/binding-win32-arm64-msvc@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.3.2.tgz#2ac75b045117301ea1fc7fd44a8428fbcec6d805"
+ integrity sha512-8volxqn9vps8XKj0DTRk/4d5TXL+vkaBRWF7CzzdfZYm/smvrdz2Iw7VmcACA7XaS41xqeTtrdq6CmaxC/4CFg==
-"@rspack/binding-win32-ia32-msvc@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.1.1.tgz#73f7c78bb4398e009708e6523e20222967ec9568"
- integrity sha512-pgXE45ATK/Iil/oXlqaGoWZ0x3SoQk4dAjJGK7TzQuek6UEoJbLQL+W1ufe/iUxz67ICAmUvq5NH2ftOhEE2SA==
+"@rspack/binding-win32-ia32-msvc@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.3.2.tgz#5f7c1d6182c7d442bd39e0e3b831e92c662242aa"
+ integrity sha512-jTIiV4pt62xK3qNqI88F8rM+ynM36UmbZ8CRFqXRHdC+Cx/dUmk83IGQr9DNvjM7we7BxUm3Shmi1f0KyZrBKw==
-"@rspack/binding-win32-x64-msvc@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.1.tgz#5b65e210d9a0dc042059399469ca5beeea54b1ee"
- integrity sha512-z/kdbB+uhMi+H4podjTE7bfUpahACUuPOZPUtAAA6PMgRyiigBTK5UFYN35D30MONwZP4yNiLvPjurwiLw7EpA==
+"@rspack/binding-win32-x64-msvc@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.3.2.tgz#1e11d6233aa9098952b6a5893beb02fbf9fc26f1"
+ integrity sha512-DfQmL7LsqD7KEZv8/z0p6AkwQAGlv5fvl5X5z4bxyRc4JMvEPBxY8lW9iK5Hk66ECzERUI2HcQ0JbRD/e4oL8A==
-"@rspack/binding@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/binding/-/binding-1.1.1.tgz#e37e0c34e723655775d33a72ba663c84a1310c0f"
- integrity sha512-BRFliHbErqWrUo9X9bdik9WTRi6EgrJSQbbUiVeIYgW4gzYdfHUohgTkWo2Byu36LZolKrEjq/Uq2A8q/tc0YA==
+"@rspack/binding@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/binding/-/binding-1.3.2.tgz#75aea649ee0db73dca98b8d939bb0afd38bbd658"
+ integrity sha512-QK+nHPDQGv16mBpJa5vULDrqDilgiFZ/BbGCZoCZRX373R9s0Doe6DBbty+RfTJwCsalF3r8X6MdWfy7UPu6Hw==
optionalDependencies:
- "@rspack/binding-darwin-arm64" "1.1.1"
- "@rspack/binding-darwin-x64" "1.1.1"
- "@rspack/binding-linux-arm64-gnu" "1.1.1"
- "@rspack/binding-linux-arm64-musl" "1.1.1"
- "@rspack/binding-linux-x64-gnu" "1.1.1"
- "@rspack/binding-linux-x64-musl" "1.1.1"
- "@rspack/binding-win32-arm64-msvc" "1.1.1"
- "@rspack/binding-win32-ia32-msvc" "1.1.1"
- "@rspack/binding-win32-x64-msvc" "1.1.1"
+ "@rspack/binding-darwin-arm64" "1.3.2"
+ "@rspack/binding-darwin-x64" "1.3.2"
+ "@rspack/binding-linux-arm64-gnu" "1.3.2"
+ "@rspack/binding-linux-arm64-musl" "1.3.2"
+ "@rspack/binding-linux-x64-gnu" "1.3.2"
+ "@rspack/binding-linux-x64-musl" "1.3.2"
+ "@rspack/binding-win32-arm64-msvc" "1.3.2"
+ "@rspack/binding-win32-ia32-msvc" "1.3.2"
+ "@rspack/binding-win32-x64-msvc" "1.3.2"
-"@rspack/core@1.1.1":
- version "1.1.1"
- resolved "https://registry.npmmirror.com/@rspack/core/-/core-1.1.1.tgz#69f795225e31f51dff6b0ccfcebcc07accdac4c8"
- integrity sha512-khYNAho2evyc7N5mYk4K6B587ou/dN1CDCqWrSDeZZNFFQHtuEp5T3kL1ntsKY7agObQhI60osCYaxFUPs0yww==
+"@rspack/core@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmmirror.com/@rspack/core/-/core-1.3.2.tgz#622e04228b39e442b7c8178bb0b71219b3f14f28"
+ integrity sha512-QbEn1SkNW3b89KTlSkp6OHdvw3DhpL6tSdDhsOlldw3LoRBy4fx80Z9W9lmg+g+8DjTAs1Z1ysElEFtAN69AZg==
dependencies:
- "@module-federation/runtime-tools" "0.5.1"
- "@rspack/binding" "1.1.1"
+ "@module-federation/runtime-tools" "0.11.2"
+ "@rspack/binding" "1.3.2"
"@rspack/lite-tapable" "1.0.1"
- caniuse-lite "^1.0.30001616"
+ caniuse-lite "^1.0.30001707"
+ tinypool "^1.0.2"
"@rspack/lite-tapable@1.0.1":
version "1.0.1"
@@ -9040,11 +9115,11 @@
undici-types "~6.20.0"
"@types/node@>=18", "@types/node@^22.5.4":
- version "22.13.9"
- resolved "https://registry.npmmirror.com/@types/node/-/node-22.13.9.tgz#5d9a8f7a975a5bd3ef267352deb96fb13ec02eca"
- integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==
+ version "22.14.1"
+ resolved "https://registry.npmmirror.com/@types/node/-/node-22.14.1.tgz#53b54585cec81c21eee3697521e31312d6ca1e6f"
+ integrity sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==
dependencies:
- undici-types "~6.20.0"
+ undici-types "~6.21.0"
"@types/node@^12.0.2":
version "12.20.55"
@@ -9110,9 +9185,9 @@
"@types/express" "*"
"@types/pg@^8.10.9":
- version "8.11.11"
- resolved "https://registry.npmmirror.com/@types/pg/-/pg-8.11.11.tgz#3bdce0580e521a62a62339a53d110458d9eae6df"
- integrity sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==
+ version "8.11.13"
+ resolved "https://registry.npmmirror.com/@types/pg/-/pg-8.11.13.tgz#ab785528b173d9bf8623406d7611ee0c0c715914"
+ integrity sha512-6kXByGkvRvwXLuyaWzsebs2du6+XuAB2CuMsuzP7uaihQahshVgSmB22Pmh0vQMkQ1h5+PZU0d+Di1o+WpVWJg==
dependencies:
"@types/node" "*"
pg-protocol "*"
@@ -9631,6 +9706,31 @@
"@typescript-eslint/types" "6.21.0"
eslint-visitor-keys "^3.4.1"
+"@uiw/codemirror-extensions-basic-setup@4.23.10":
+ version "4.23.10"
+ resolved "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.10.tgz#e5d901e860a039ac61d955af26a12866e9dc356c"
+ integrity sha512-zpbmSeNs3OU/f/Eyd6brFnjsBUYwv2mFjWxlAsIRSwTlW+skIT60rQHFBSfsj/5UVSxSLWVeUYczN7AyXvgTGQ==
+ dependencies:
+ "@codemirror/autocomplete" "^6.0.0"
+ "@codemirror/commands" "^6.0.0"
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/lint" "^6.0.0"
+ "@codemirror/search" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+
+"@uiw/react-codemirror@4.23.10":
+ version "4.23.10"
+ resolved "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.23.10.tgz#2e34aec4f65f901ed8e9b8a22e28f2177addce69"
+ integrity sha512-AbN4eVHOL4ckRuIXpZxkzEqL/1ChVA+BSdEnAKjIB68pLQvKsVoYbiFP8zkXkYc4+Fcgq5KbAjvYqdo4ewemKw==
+ dependencies:
+ "@babel/runtime" "^7.18.6"
+ "@codemirror/commands" "^6.1.0"
+ "@codemirror/state" "^6.1.1"
+ "@codemirror/theme-one-dark" "^6.0.0"
+ "@uiw/codemirror-extensions-basic-setup" "4.23.10"
+ codemirror "^6.0.0"
+
"@umijs/ast@4.0.89":
version "4.0.89"
resolved "https://registry.npmmirror.com/@umijs/ast/-/ast-4.0.89.tgz#af7591eb9447c7adfb6f9704fd177542bae7c0d3"
@@ -10112,10 +10212,10 @@
resolved "https://registry.npmmirror.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
-"@zeit/schemas@2.6.0":
- version "2.6.0"
- resolved "https://registry.npmmirror.com/@zeit/schemas/-/schemas-2.6.0.tgz#004e8e553b4cd53d538bd38eac7bcbf58a867fe3"
- integrity sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg==
+"@zeit/schemas@2.36.0":
+ version "2.36.0"
+ resolved "https://registry.npmmirror.com/@zeit/schemas/-/schemas-2.36.0.tgz#7a1b53f4091e18d0b404873ea3e3c83589c765f2"
+ integrity sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==
JSONStream@^1.0.4:
version "1.3.5"
@@ -10322,7 +10422,17 @@ ajv-keywords@^5.1.0:
dependencies:
fast-deep-equal "^3.1.3"
-ajv@6.12.6, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6:
+ajv@8.12.0:
+ version "8.12.0"
+ resolved "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
+ integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ json-schema-traverse "^1.0.0"
+ require-from-string "^2.0.2"
+ uri-js "^4.2.2"
+
+ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6:
version "6.12.6"
resolved "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -10421,7 +10531,7 @@ ansi-align@^2.0.0:
dependencies:
string-width "^2.0.0"
-ansi-align@^3.0.0:
+ansi-align@^3.0.0, ansi-align@^3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==
@@ -10696,7 +10806,7 @@ aproba@^1.0.3, aproba@^1.1.1:
resolved "https://registry.npmmirror.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
-arch@^2.1.1:
+arch@^2.1.1, arch@^2.2.0:
version "2.2.0"
resolved "https://registry.npmmirror.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
@@ -10800,21 +10910,16 @@ are-we-there-yet@~1.1.2:
delegates "^1.0.0"
readable-stream "^2.0.6"
-arg@2.0.0:
- version "2.0.0"
- resolved "https://registry.npmmirror.com/arg/-/arg-2.0.0.tgz#c06e7ff69ab05b3a4a03ebe0407fac4cba657545"
- integrity sha512-XxNTUzKnz1ctK3ZIcI2XUPlD96wbHP2nGqkPKpvk/HNRlPveYrXIVSTk9m3LcqOgDPg3B1nMvdV/K8wZd7PG4w==
+arg@5.0.2, arg@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
+ integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
-arg@^5.0.0:
- version "5.0.2"
- resolved "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
- integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
-
argparse@^1.0.7, argparse@~1.0.9:
version "1.0.10"
resolved "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -11139,14 +11244,6 @@ async-each@^1.0.1:
resolved "https://registry.npmmirror.com/async-each/-/async-each-1.0.6.tgz#52f1d9403818c179b7561e11a5d1b77eb2160e77"
integrity sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==
-async-listener@^0.6.0:
- version "0.6.10"
- resolved "https://registry.npmmirror.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc"
- integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==
- dependencies:
- semver "^5.3.0"
- shimmer "^1.1.0"
-
async-mutex@^0.3.2:
version "0.3.2"
resolved "https://registry.npmmirror.com/async-mutex/-/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df"
@@ -11185,6 +11282,11 @@ async@^3.2.0, async@^3.2.3, async@^3.2.4, async@~3.2.0:
resolved "https://registry.npmmirror.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
+async@~3.2.6:
+ version "3.2.6"
+ resolved "https://registry.npmmirror.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
+ integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
+
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -11288,13 +11390,6 @@ axios@^0.18.1:
follow-redirects "1.5.10"
is-buffer "^2.0.2"
-axios@^0.21.0:
- version "0.21.4"
- resolved "https://registry.npmmirror.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
- integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
- dependencies:
- follow-redirects "^1.14.0"
-
axios@^1.7.0:
version "1.7.7"
resolved "https://registry.npmmirror.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
@@ -11604,9 +11699,9 @@ bl@^4.0.3, bl@^4.1.0:
readable-stream "^3.4.0"
bl@^6.0.11:
- version "6.0.20"
- resolved "https://registry.npmmirror.com/bl/-/bl-6.0.20.tgz#70527afc27d2158b88db55a99ad8813ab7d82914"
- integrity sha512-JMP0loH6ApbpT4Aa9oU5NqAkdDvcyc8koeuK8i5mYoBCVj3XCXG0uweGNN2m6DqaCO2yRHdm+MjCeTsR5VsmcA==
+ version "6.1.0"
+ resolved "https://registry.npmmirror.com/bl/-/bl-6.1.0.tgz#cc35ce7a2e8458caa8c8fb5deeed6537b73e4504"
+ integrity sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw==
dependencies:
"@types/readable-stream" "^4.0.0"
buffer "^6.0.3"
@@ -11691,19 +11786,19 @@ bowser@^2.11.0:
resolved "https://registry.npmmirror.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
-boxen@5.1.2:
- version "5.1.2"
- resolved "https://registry.npmmirror.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50"
- integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==
+boxen@7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmmirror.com/boxen/-/boxen-7.0.0.tgz#9e5f8c26e716793fc96edcf7cf754cdf5e3fbf32"
+ integrity sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==
dependencies:
- ansi-align "^3.0.0"
- camelcase "^6.2.0"
- chalk "^4.1.0"
- cli-boxes "^2.2.1"
- string-width "^4.2.2"
- type-fest "^0.20.2"
- widest-line "^3.1.0"
- wrap-ansi "^7.0.0"
+ ansi-align "^3.0.1"
+ camelcase "^7.0.0"
+ chalk "^5.0.1"
+ cli-boxes "^3.0.0"
+ string-width "^5.1.2"
+ type-fest "^2.13.0"
+ widest-line "^4.0.1"
+ wrap-ansi "^8.0.1"
boxen@^1.2.1:
version "1.3.0"
@@ -12289,6 +12384,11 @@ camelcase@^5.0.0, camelcase@^5.3.1:
resolved "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+camelcase@^7.0.0:
+ version "7.0.1"
+ resolved "https://registry.npmmirror.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048"
+ integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==
+
camelize@^1.0.0:
version "1.0.1"
resolved "https://registry.npmmirror.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3"
@@ -12299,16 +12399,21 @@ caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001565:
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz#b4e5c1fa786f733ab78fc70f592df6b3f23244ca"
integrity sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==
-caniuse-lite@^1.0.30001616, caniuse-lite@^1.0.30001688:
- version "1.0.30001690"
- resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8"
- integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==
-
caniuse-lite@^1.0.30001646:
version "1.0.30001651"
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138"
integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==
+caniuse-lite@^1.0.30001688:
+ version "1.0.30001690"
+ resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8"
+ integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==
+
+caniuse-lite@^1.0.30001707:
+ version "1.0.30001713"
+ resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz#6b33a8857e6c7dcb41a0caa2dd0f0489c823a52d"
+ integrity sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==
+
capture-stack-trace@^1.0.0:
version "1.0.2"
resolved "https://registry.npmmirror.com/capture-stack-trace/-/capture-stack-trace-1.0.2.tgz#1c43f6b059d4249e7f3f8724f15f048b927d3a8a"
@@ -12372,14 +12477,12 @@ chainsaw@~0.1.0:
dependencies:
traverse ">=0.3.0 <0.4"
-chalk@2.4.1:
- version "2.4.1"
- resolved "https://registry.npmmirror.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
- integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==
+chalk-template@0.4.0:
+ version "0.4.0"
+ resolved "https://registry.npmmirror.com/chalk-template/-/chalk-template-0.4.0.tgz#692c034d0ed62436b9062c1707fadcd0f753204b"
+ integrity sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==
dependencies:
- ansi-styles "^3.2.1"
- escape-string-regexp "^1.0.5"
- supports-color "^5.3.0"
+ chalk "^4.1.2"
chalk@2.4.2, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
@@ -12398,6 +12501,11 @@ chalk@3.0.0, chalk@^3.0.0, chalk@~3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
+chalk@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmmirror.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6"
+ integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==
+
chalk@5.3.0, chalk@^5.3.0:
version "5.3.0"
resolved "https://registry.npmmirror.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
@@ -12422,6 +12530,11 @@ chalk@^4, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
+chalk@^5.0.1:
+ version "5.4.1"
+ resolved "https://registry.npmmirror.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8"
+ integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==
+
character-entities-html4@^2.0.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
@@ -12553,6 +12666,11 @@ chownr@^2.0.0:
resolved "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
+chownr@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4"
+ integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==
+
ci-info@^1.5.0:
version "1.6.0"
resolved "https://registry.npmmirror.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
@@ -12630,11 +12748,16 @@ cli-boxes@^1.0.0:
resolved "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
integrity sha512-3Fo5wu8Ytle8q9iCzS4D2MWVL2X7JVWRiS1BnXbTFDhS9c/REkM9vd1AmabsoZoY5/dGi5TT9iKL8Kb6DeBRQg==
-cli-boxes@^2.2.0, cli-boxes@^2.2.1:
+cli-boxes@^2.2.0:
version "2.2.1"
resolved "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
+cli-boxes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145"
+ integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==
+
cli-cursor@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
@@ -12722,6 +12845,15 @@ clipboardy@2.3.0:
execa "^1.0.0"
is-wsl "^2.1.1"
+clipboardy@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/clipboardy/-/clipboardy-3.0.0.tgz#f3876247404d334c9ed01b6f269c11d09a5e3092"
+ integrity sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==
+ dependencies:
+ arch "^2.2.0"
+ execa "^5.1.1"
+ is-wsl "^2.2.0"
+
cliui@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
@@ -12858,7 +12990,7 @@ code-point-at@^1.0.0:
resolved "https://registry.npmmirror.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==
-codemirror@^6.0.1:
+codemirror@^6.0.0, codemirror@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29"
integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==
@@ -13117,26 +13249,39 @@ compress-commons@^5.0.1:
normalize-path "^3.0.0"
readable-stream "^3.6.0"
-compressible@~2.0.14:
+compressible@~2.0.16, compressible@~2.0.18:
version "2.0.18"
resolved "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
dependencies:
mime-db ">= 1.43.0 < 2"
-compression@1.7.3:
- version "1.7.3"
- resolved "https://registry.npmmirror.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db"
- integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==
+compression@1.7.4:
+ version "1.7.4"
+ resolved "https://registry.npmmirror.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
+ integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
dependencies:
accepts "~1.3.5"
bytes "3.0.0"
- compressible "~2.0.14"
+ compressible "~2.0.16"
debug "2.6.9"
- on-headers "~1.0.1"
+ on-headers "~1.0.2"
safe-buffer "5.1.2"
vary "~1.1.2"
+compression@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.npmmirror.com/compression/-/compression-1.8.0.tgz#09420efc96e11a0f44f3a558de59e321364180f7"
+ integrity sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==
+ dependencies:
+ bytes "3.1.2"
+ compressible "~2.0.18"
+ debug "2.6.9"
+ negotiator "~0.6.4"
+ on-headers "~1.0.2"
+ safe-buffer "5.2.1"
+ vary "~1.1.2"
+
compute-scroll-into-view@^3.0.2:
version "3.1.0"
resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87"
@@ -13292,14 +13437,6 @@ content-type@^1.0.2, content-type@^1.0.4, content-type@~1.0.4, content-type@~1.0
resolved "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
-continuation-local-storage@^3.2.1:
- version "3.2.1"
- resolved "https://registry.npmmirror.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb"
- integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==
- dependencies:
- async-listener "^0.6.0"
- emitter-listener "^1.1.1"
-
contour_plot@^0.0.1:
version "0.0.1"
resolved "https://registry.npmmirror.com/contour_plot/-/contour_plot-0.0.1.tgz#475870f032b8e338412aa5fc507880f0bf495c77"
@@ -14443,7 +14580,7 @@ dayjs-timezone-iana-plugin@=0.1.0:
resolved "https://registry.npmmirror.com/dayjs-timezone-iana-plugin/-/dayjs-timezone-iana-plugin-0.1.0.tgz#216613f6ec80106ab8be025cf5935018c901e997"
integrity sha512-xc8cIZmi4oKr2nfu41I/FDWZKa8n8YaRMxSz9MrpXTNo8c6ZsjZuIoy5RPNmLXPqntFuITWI8obB7lUA+CdzGQ==
-dayjs@1.11.13, dayjs@=1.11.11, dayjs@^1.11.10, dayjs@^1.11.11, dayjs@^1.11.7, dayjs@^1.11.8, dayjs@^1.11.9, dayjs@^1.8.34, dayjs@^1.9.1, dayjs@~1.11.5, dayjs@~1.8.24:
+dayjs@1.11.13, dayjs@=1.11.11, dayjs@^1.11.10, dayjs@^1.11.11, dayjs@^1.11.7, dayjs@^1.11.8, dayjs@^1.11.9, dayjs@^1.8.34, dayjs@^1.9.1, dayjs@~1.11.13, dayjs@~1.8.24:
version "1.11.13"
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
@@ -14502,6 +14639,13 @@ debug@^3.1.0, debug@^3.2.6, debug@^3.2.7:
dependencies:
ms "^2.1.1"
+debug@^4.3.7:
+ version "4.4.0"
+ resolved "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
+ integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+ dependencies:
+ ms "^2.1.3"
+
debug@~4.3.2, debug@~4.3.4:
version "4.3.7"
resolved "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
@@ -15423,13 +15567,6 @@ elliptic@^6.5.3, elliptic@^6.5.4:
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
-emitter-listener@^1.1.1:
- version "1.1.2"
- resolved "https://registry.npmmirror.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
- integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
- dependencies:
- shimmer "^1.2.0"
-
emittery@^0.13.0:
version "0.13.1"
resolved "https://registry.npmmirror.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
@@ -16424,11 +16561,6 @@ eventemitter2@^6.3.1:
resolved "https://registry.npmmirror.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125"
integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==
-eventemitter2@~0.4.14:
- version "0.4.14"
- resolved "https://registry.npmmirror.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab"
- integrity sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==
-
eventemitter3@^2.0.0, eventemitter3@^2.0.3:
version "2.0.3"
resolved "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
@@ -16697,6 +16829,13 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
+extrareqp2@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmmirror.com/extrareqp2/-/extrareqp2-1.0.0.tgz#aaf8ad1495d723f71276b0eab041c061aa21f035"
+ integrity sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==
+ dependencies:
+ follow-redirects "^1.14.0"
+
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -16767,7 +16906,7 @@ fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1:
merge2 "^1.3.0"
micromatch "^4.0.4"
-fast-json-patch@^3.0.0-1:
+fast-json-patch@^3.1.0:
version "3.1.1"
resolved "https://registry.npmmirror.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947"
integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==
@@ -16807,13 +16946,6 @@ fast-uri@^3.0.1:
resolved "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241"
integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==
-fast-url-parser@1.1.3:
- version "1.1.3"
- resolved "https://registry.npmmirror.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
- integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==
- dependencies:
- punycode "^1.3.2"
-
fast-xml-parser@4.2.5:
version "4.2.5"
resolved "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz#a6747a09296a6cb34f2ae634019bf1738f3b421f"
@@ -17414,6 +17546,15 @@ fs-extra@^11.1.1:
jsonfile "^6.0.1"
universalify "^2.0.0"
+fs-extra@^11.3.0:
+ version "11.3.0"
+ resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
+ integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
+
fs-extra@^3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
@@ -17994,7 +18135,7 @@ glob@^10.2.2, glob@^10.3.10:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
-glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3, glob@~7.2.3:
+glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3, glob@~7.2.3:
version "7.2.3"
resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -18959,7 +19100,7 @@ http-proxy-agent@^4.0.1:
agent-base "6"
debug "4"
-http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.2:
+http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1, http-proxy-agent@^7.0.2:
version "7.0.2"
resolved "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
@@ -18997,7 +19138,7 @@ https-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
-https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1:
+https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.3:
version "7.0.6"
resolved "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
@@ -19021,7 +19162,7 @@ httpx@^2.2.0, httpx@^2.2.6:
"@types/node" "^20"
debug "^4.1.1"
-httpx@^2.3.2:
+httpx@^2.3.2, httpx@^2.3.3:
version "2.3.3"
resolved "https://registry.npmmirror.com/httpx/-/httpx-2.3.3.tgz#353d3d9161b7cc2be4c638873f2fe6b319354c89"
integrity sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==
@@ -19372,9 +19513,9 @@ invert-kv@^1.0.0:
integrity sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==
ioredis@^5.4.1:
- version "5.5.0"
- resolved "https://registry.npmmirror.com/ioredis/-/ioredis-5.5.0.tgz#ff2332e125ca2ac8e15472ddd14ecdffa6484a2a"
- integrity sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==
+ version "5.6.1"
+ resolved "https://registry.npmmirror.com/ioredis/-/ioredis-5.6.1.tgz#1ed7dc9131081e77342503425afceaf7357ae599"
+ integrity sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==
dependencies:
"@ioredis/commands" "^1.1.1"
cluster-key-slot "^1.1.0"
@@ -19883,6 +20024,11 @@ is-plain-object@^5.0.0:
resolved "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
+is-port-reachable@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmmirror.com/is-port-reachable/-/is-port-reachable-4.0.0.tgz#dac044091ef15319c8ab2f34604d8794181f8c2d"
+ integrity sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==
+
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
@@ -20377,7 +20523,7 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
-js-yaml@^4.1.0:
+js-yaml@^4.1.0, js-yaml@~4.1.0:
version "4.1.0"
resolved "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
@@ -20940,11 +21086,6 @@ lazy-cache@^1.0.3, lazy-cache@^1.0.4:
resolved "https://registry.npmmirror.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
integrity sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==
-lazy@~1.0.11:
- version "1.0.11"
- resolved "https://registry.npmmirror.com/lazy/-/lazy-1.0.11.tgz#daa068206282542c088288e975c297c1ae77b690"
- integrity sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==
-
lazystream@^1.0.0:
version "1.0.1"
resolved "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638"
@@ -21524,11 +21665,6 @@ lodash@4.17.21, lodash@4.x, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, l
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-log-driver@^1.2.7:
- version "1.2.7"
- resolved "https://registry.npmmirror.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8"
- integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==
-
log-symbols@^2.1.0:
version "2.2.0"
resolved "https://registry.npmmirror.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
@@ -21858,9 +21994,9 @@ mariadb@^2.5.6:
please-upgrade-node "^3.2.0"
mariadb@^3.3.0:
- version "3.4.0"
- resolved "https://registry.npmmirror.com/mariadb/-/mariadb-3.4.0.tgz#3dc2e918f000b39df9e1c2139604a4b2ebaa8d1c"
- integrity sha512-hdRPcAzs+MTxK5VG1thBW18gGTlw6yWBe9YnLB65GLo7q0fO5DWsgomIevV/pXSaWRmD3qi6ka4oSFRTExRiEQ==
+ version "3.4.1"
+ resolved "https://registry.npmmirror.com/mariadb/-/mariadb-3.4.1.tgz#7444ee942410bd48f0a713d02b1539785c3c83d8"
+ integrity sha512-h2q6JREwHrzPx7bVVdHCkMGCbCXZmUk6WhDPcNrmhgGoYe15Cbs24Giq/m43oXFO/6Gqz4MTaRrnipWCgtgetQ==
dependencies:
"@types/geojson" "^7946.0.14"
"@types/node" "^22.5.4"
@@ -22692,13 +22828,6 @@ minimalistic-crypto-utils@^1.0.1:
resolved "https://registry.npmmirror.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==
-minimatch@3.0.4:
- version "3.0.4"
- resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
- integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
- dependencies:
- brace-expansion "^1.1.7"
-
minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -22838,7 +22967,7 @@ minipass@^5.0.0:
resolved "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
-"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.2, minipass@^7.0.3, minipass@^7.1.2:
+"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.2, minipass@^7.0.3, minipass@^7.0.4, minipass@^7.1.2:
version "7.1.2"
resolved "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
@@ -22858,6 +22987,13 @@ minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2:
minipass "^3.0.0"
yallist "^4.0.0"
+minizlib@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.npmmirror.com/minizlib/-/minizlib-3.0.2.tgz#f33d638eb279f664439aa38dc5f91607468cb574"
+ integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==
+ dependencies:
+ minipass "^7.1.2"
+
mississippi@^1.2.0, mississippi@^1.3.0:
version "1.3.1"
resolved "https://registry.npmmirror.com/mississippi/-/mississippi-1.3.1.tgz#2a8bb465e86550ac8b36a7b6f45599171d78671e"
@@ -22929,6 +23065,11 @@ mkdirp@1.0.4, mkdirp@^1.0.3, mkdirp@^1.0.4:
dependencies:
minimist "^1.2.6"
+mkdirp@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmmirror.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
+ integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
+
mlly@^1.2.0, mlly@^1.4.2:
version "1.4.2"
resolved "https://registry.npmmirror.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e"
@@ -22976,9 +23117,9 @@ moment-timezone@^0.5.34, moment-timezone@^0.5.40, moment-timezone@^0.5.43:
moment "^2.29.4"
moment-timezone@^0.5.45:
- version "0.5.47"
- resolved "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.47.tgz#d4d1a21b78372d914d6d69ae285454732a429749"
- integrity sha512-UbNt/JAWS0m/NJOebR0QMRHBk0hu03r5dx9GK8Cs0AS3I81yDcOc9k+DytPItgVvBP7J6Mf6U2n3BPAacAV9oA==
+ version "0.5.48"
+ resolved "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz#111727bb274734a518ae154b5ca589283f058967"
+ integrity sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==
dependencies:
moment "^2.29.4"
@@ -23171,7 +23312,12 @@ nanoid@^2.1.0:
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
-nanoid@^3.3.6, nanoid@^3.3.7:
+nanoid@^3.3.11, nanoid@^3.3.6:
+ version "3.3.11"
+ resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
+ integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
+
+nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
@@ -23230,7 +23376,7 @@ negotiator@0.6.3, negotiator@^0.6.2:
resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
-negotiator@^0.6.3:
+negotiator@^0.6.3, negotiator@~0.6.4:
version "0.6.4"
resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
@@ -23442,6 +23588,11 @@ node-xlsx@^0.16.1:
buffer-from "^1.1.1"
xlsx "^0.17.0"
+nodejs-snowflake@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmmirror.com/nodejs-snowflake/-/nodejs-snowflake-2.0.1.tgz#d82fb1c7398404c5ac984d6f1cdfb1b5aef7bd80"
+ integrity sha512-zMfDorNNsJm1OWWx/OUUGVT0bQ3TwC2ti4URD8UjnpffVLtLpy3eBDIaV5TnLs91YrRhwBD6L0StqU6YtnZt9A==
+
nodemailer-mock@^1.5.11:
version "1.5.12"
resolved "https://registry.npmmirror.com/nodemailer-mock/-/nodemailer-mock-1.5.12.tgz#607ec52a46a05fc1ed96585d830aad26407c7696"
@@ -23703,14 +23854,6 @@ nprogress@^0.2.0:
resolved "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==
-nssocket@0.6.0:
- version "0.6.0"
- resolved "https://registry.npmmirror.com/nssocket/-/nssocket-0.6.0.tgz#59f96f6ff321566f33c70f7dbeeecdfdc07154fa"
- integrity sha512-a9GSOIql5IqgWJR3F/JXG4KpJTA3Z53Cj0MeMvGpglytB1nxE4PdFNC0jINe27CS7cGivoynwc054EzCcT3M3w==
- dependencies:
- eventemitter2 "~0.4.14"
- lazy "~1.0.11"
-
nth-check@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
@@ -23924,9 +24067,9 @@ oidc-token-hash@^5.0.3:
integrity sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==
ollama@^0.5.12:
- version "0.5.14"
- resolved "https://registry.npmmirror.com/ollama/-/ollama-0.5.14.tgz#bf1b72998491636ee89766bb0e3163e3c50a6970"
- integrity sha512-pvOuEYa2WkkAumxzJP0RdEYHkbZ64AYyyUszXVX7ruLvk5L+EiO2G71da2GqEQ4IAk4j6eLoUbGk5arzFT1wJA==
+ version "0.5.15"
+ resolved "https://registry.npmmirror.com/ollama/-/ollama-0.5.15.tgz#34a4549af3c4b819d39c81d313d911f6b8a9ef6e"
+ integrity sha512-TSaZSJyP7MQJFjSmmNsoJiriwa3U+/UJRw6+M8aucs5dTsaWNZsBIGpDb5rXnW6nXxJBB/z79gZY8IaiIQgelQ==
dependencies:
whatwg-fetch "^3.6.20"
@@ -23957,7 +24100,7 @@ on-finished@~2.3.0:
dependencies:
ee-first "1.1.1"
-on-headers@~1.0.1:
+on-headers@~1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
@@ -24003,9 +24146,9 @@ only@~0.0.2:
integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==
open@^10.1.0:
- version "10.1.0"
- resolved "https://registry.npmmirror.com/open/-/open-10.1.0.tgz#a7795e6e5d519abe4286d9937bb24b51122598e1"
- integrity sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==
+ version "10.1.1"
+ resolved "https://registry.npmmirror.com/open/-/open-10.1.1.tgz#5fd814699e47ae3e1a09962d39f4f4441cae6c22"
+ integrity sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==
dependencies:
default-browser "^5.2.1"
define-lazy-prop "^3.0.0"
@@ -24766,10 +24909,10 @@ path-to-regexp@1.7.0:
dependencies:
isarray "0.0.1"
-path-to-regexp@2.2.1:
- version "2.2.1"
- resolved "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45"
- integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==
+path-to-regexp@3.3.0:
+ version "3.3.0"
+ resolved "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b"
+ integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==
path-to-regexp@6.2.2:
version "6.2.2"
@@ -24893,9 +25036,9 @@ pg-pool@^3.6.1:
integrity sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==
pg-protocol@*:
- version "1.7.1"
- resolved "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.7.1.tgz#aad61a6f927b51e89dcf721408b76c0e536d43dc"
- integrity sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==
+ version "1.8.0"
+ resolved "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.8.0.tgz#c707101dd07813868035a44571488e4b98639d48"
+ integrity sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==
pg-protocol@^1.6.0:
version "1.6.0"
@@ -25163,27 +25306,28 @@ pm2-sysmonit@^1.2.8:
systeminformation "^5.7"
tx2 "~1.0.4"
-pm2@^5.2.0:
- version "5.3.0"
- resolved "https://registry.npmmirror.com/pm2/-/pm2-5.3.0.tgz#06850810f77cd98495ae1c66fbdd028a8fb5899e"
- integrity sha512-xscmQiAAf6ArVmKhjKTeeN8+Td7ZKnuZFFPw1DGkdFPR/0Iyx+m+1+OpCdf9+HQopX3VPc9/wqPQHqVOfHum9w==
+pm2@^6.0.5:
+ version "6.0.5"
+ resolved "https://registry.npmmirror.com/pm2/-/pm2-6.0.5.tgz#a6eeedeb7d3bf59c145b2c00fb57ce1fbf1fa7db"
+ integrity sha512-+O43WPaEiwYbm6/XSpAOO1Rtya/Uof0n7x8hJZGfwIuepesNTIVArpZh4KqFfze0cvvqZMr0maTW3ifhvmyeMQ==
dependencies:
- "@pm2/agent" "~2.0.0"
- "@pm2/io" "~5.0.0"
- "@pm2/js-api" "~0.6.7"
+ "@pm2/agent" "~2.1.1"
+ "@pm2/io" "~6.1.0"
+ "@pm2/js-api" "~0.8.0"
"@pm2/pm2-version-check" latest
- async "~3.2.0"
+ async "~3.2.6"
blessed "0.1.81"
chalk "3.0.0"
chokidar "^3.5.3"
cli-tableau "^2.0.0"
commander "2.15.1"
croner "~4.1.92"
- dayjs "~1.11.5"
- debug "^4.3.1"
+ dayjs "~1.11.13"
+ debug "^4.3.7"
enquirer "2.3.6"
eventemitter2 "5.0.1"
fclone "1.0.11"
+ js-yaml "~4.1.0"
mkdirp "1.0.4"
needle "2.4.0"
pidusage "~3.0"
@@ -25192,11 +25336,10 @@ pm2@^5.2.0:
pm2-deploy "~1.0.2"
pm2-multimeter "^0.1.2"
promptly "^2"
- semver "^7.2"
+ semver "^7.6.2"
source-map-support "0.5.21"
sprintf-js "1.1.2"
vizion "~2.2.1"
- yamljs "0.3.0"
optionalDependencies:
pm2-sysmonit "^1.2.8"
@@ -25836,9 +25979,9 @@ postgres-array@~2.0.0:
integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
postgres-array@~3.0.1:
- version "3.0.2"
- resolved "https://registry.npmmirror.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98"
- integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==
+ version "3.0.4"
+ resolved "https://registry.npmmirror.com/postgres-array/-/postgres-array-3.0.4.tgz#4efcaf4d2c688d8bcaa8620ed13f35f299f7528c"
+ integrity sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==
postgres-bytea@~1.0.0:
version "1.0.0"
@@ -26116,15 +26259,15 @@ proxy-addr@~2.0.7:
forwarded "0.2.0"
ipaddr.js "1.9.1"
-proxy-agent@~6.3.0:
- version "6.3.1"
- resolved "https://registry.npmmirror.com/proxy-agent/-/proxy-agent-6.3.1.tgz#40e7b230552cf44fd23ffaf7c59024b692612687"
- integrity sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==
+proxy-agent@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.npmmirror.com/proxy-agent/-/proxy-agent-6.4.0.tgz#b4e2dd51dee2b377748aef8d45604c2d7608652d"
+ integrity sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==
dependencies:
agent-base "^7.0.2"
debug "^4.3.4"
- http-proxy-agent "^7.0.0"
- https-proxy-agent "^7.0.2"
+ http-proxy-agent "^7.0.1"
+ https-proxy-agent "^7.0.3"
lru-cache "^7.14.1"
pac-proxy-agent "^7.0.1"
proxy-from-env "^1.1.0"
@@ -26200,7 +26343,7 @@ punycode.js@^2.3.1:
resolved "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
-punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1:
+punycode@^1.2.4, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.npmmirror.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
@@ -26224,15 +26367,6 @@ qrcode@^1.5.4:
pngjs "^5.0.0"
yargs "^15.3.1"
-qrcode@^1.5.4:
- version "1.5.4"
- resolved "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88"
- integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
- dependencies:
- dijkstrajs "^1.0.1"
- pngjs "^5.0.0"
- yargs "^15.3.1"
-
qs@6.13.0:
version "6.13.0"
resolved "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
@@ -28439,13 +28573,18 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
-semver@^7.1.1, semver@^7.1.3, semver@^7.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4:
+semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4:
version "7.6.0"
resolved "https://registry.npmmirror.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
dependencies:
lru-cache "^6.0.0"
+semver@^7.6.2, semver@^7.7.1:
+ version "7.7.1"
+ resolved "https://registry.npmmirror.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
+ integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
+
semver@^7.6.3:
version "7.6.3"
resolved "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
@@ -28510,9 +28649,9 @@ sequelize@^6.26.0:
wkx "^0.5.0"
sequelize@^6.35.0:
- version "6.37.5"
- resolved "https://registry.npmmirror.com/sequelize/-/sequelize-6.37.5.tgz#2711ab97d0e0fe49c652368946a7312eb0f11cd7"
- integrity sha512-10WA4poUb3XWnUROThqL2Apq9C2NhyV1xHPMZuybNMCucDsbbFuKg51jhmyvvAUyUqCiimwTZamc3AHhMoBr2Q==
+ version "6.37.7"
+ resolved "https://registry.npmmirror.com/sequelize/-/sequelize-6.37.7.tgz#55a6f8555ae76c1fbd4bce76b2ac5fcc0a1e6eb6"
+ integrity sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==
dependencies:
"@types/debug" "^4.1.8"
"@types/validator" "^13.7.17"
@@ -28531,32 +28670,17 @@ sequelize@^6.35.0:
validator "^13.9.0"
wkx "^0.5.0"
-serve-handler@6.1.3:
- version "6.1.3"
- resolved "https://registry.npmmirror.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8"
- integrity sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w==
+serve-handler@6.1.6, serve-handler@^6.1.6:
+ version "6.1.6"
+ resolved "https://registry.npmmirror.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1"
+ integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==
dependencies:
bytes "3.0.0"
content-disposition "0.5.2"
- fast-url-parser "1.1.3"
- mime-types "2.1.18"
- minimatch "3.0.4"
- path-is-inside "1.0.2"
- path-to-regexp "2.2.1"
- range-parser "1.2.0"
-
-serve-handler@^6.1.5:
- version "6.1.5"
- resolved "https://registry.npmmirror.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375"
- integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==
- dependencies:
- bytes "3.0.0"
- content-disposition "0.5.2"
- fast-url-parser "1.1.3"
mime-types "2.1.18"
minimatch "3.1.2"
path-is-inside "1.0.2"
- path-to-regexp "2.2.1"
+ path-to-regexp "3.3.0"
range-parser "1.2.0"
serve-static@1.16.2:
@@ -28569,20 +28693,22 @@ serve-static@1.16.2:
parseurl "~1.3.3"
send "0.19.0"
-serve@^13.0.2:
- version "13.0.4"
- resolved "https://registry.npmmirror.com/serve/-/serve-13.0.4.tgz#fc4466dc84b3e4a6cb622247c85ed8afe4b88820"
- integrity sha512-Lj8rhXmphJCRQVv5qwu0NQZ2h+0MrRyRJxDZu5y3qLH2i/XY6a0FPj/VmjMUdkJb672MBfE8hJ274PU6JzBd0Q==
+serve@^14.2.4:
+ version "14.2.4"
+ resolved "https://registry.npmmirror.com/serve/-/serve-14.2.4.tgz#ba4c425c3c965f496703762e808f34b913f42fb0"
+ integrity sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==
dependencies:
- "@zeit/schemas" "2.6.0"
- ajv "6.12.6"
- arg "2.0.0"
- boxen "5.1.2"
- chalk "2.4.1"
- clipboardy "2.3.0"
- compression "1.7.3"
- serve-handler "6.1.3"
- update-check "1.5.2"
+ "@zeit/schemas" "2.36.0"
+ ajv "8.12.0"
+ arg "5.0.2"
+ boxen "7.0.0"
+ chalk "5.0.1"
+ chalk-template "0.4.0"
+ clipboardy "3.0.0"
+ compression "1.7.4"
+ is-port-reachable "4.0.0"
+ serve-handler "6.1.6"
+ update-check "1.5.4"
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
@@ -28704,7 +28830,7 @@ shell-quote@^1.7.3:
resolved "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
-shimmer@^1.1.0, shimmer@^1.2.0, shimmer@^1.2.1:
+shimmer@^1.2.0, shimmer@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
@@ -29366,11 +29492,6 @@ stop-iteration-iterator@^1.0.0:
dependencies:
internal-slot "^1.0.4"
-stoppable@^1.1.0:
- version "1.1.0"
- resolved "https://registry.npmmirror.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
- integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==
-
stream-browserify@3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f"
@@ -29483,7 +29604,7 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -30106,18 +30227,6 @@ tar-stream@^3.1.5:
fast-fifo "^1.2.0"
streamx "^2.15.0"
-tar@6.1.11:
- version "6.1.11"
- resolved "https://registry.npmmirror.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
- integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
- dependencies:
- chownr "^2.0.0"
- fs-minipass "^2.0.0"
- minipass "^3.0.0"
- minizlib "^2.1.1"
- mkdirp "^1.0.3"
- yallist "^4.0.0"
-
tar@^4.4.12:
version "4.4.19"
resolved "https://registry.npmmirror.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3"
@@ -30131,7 +30240,7 @@ tar@^4.4.12:
safe-buffer "^5.2.1"
yallist "^3.1.1"
-tar@^6.0.2, tar@^6.1.0, tar@^6.1.11, tar@^6.1.13, tar@^6.1.2, tar@^6.2.0:
+tar@^6.0.2, tar@^6.1.0, tar@^6.1.11, tar@^6.1.2:
version "6.2.0"
resolved "https://registry.npmmirror.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73"
integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==
@@ -30143,7 +30252,7 @@ tar@^6.0.2, tar@^6.1.0, tar@^6.1.11, tar@^6.1.13, tar@^6.1.2, tar@^6.2.0:
mkdirp "^1.0.3"
yallist "^4.0.0"
-tar@^6.2.1:
+tar@^6.1.13, tar@^6.2.1:
version "6.2.1"
resolved "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
@@ -30155,6 +30264,18 @@ tar@^6.2.1:
mkdirp "^1.0.3"
yallist "^4.0.0"
+tar@^7.4.3:
+ version "7.4.3"
+ resolved "https://registry.npmmirror.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571"
+ integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==
+ dependencies:
+ "@isaacs/fs-minipass" "^4.0.0"
+ chownr "^3.0.0"
+ minipass "^7.1.2"
+ minizlib "^3.0.1"
+ mkdirp "^3.0.1"
+ yallist "^5.0.0"
+
tedious@^18.2.4:
version "18.6.1"
resolved "https://registry.npmmirror.com/tedious/-/tedious-18.6.1.tgz#1c4a3f06c891be67a032117e2e25193286d44496"
@@ -30360,6 +30481,11 @@ tinypool@^0.8.3:
resolved "https://registry.npmmirror.com/tinypool/-/tinypool-0.8.4.tgz#e217fe1270d941b39e98c625dcecebb1408c9aa8"
integrity sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==
+tinypool@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2"
+ integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==
+
tinyspy@^2.2.0:
version "2.2.0"
resolved "https://registry.npmmirror.com/tinyspy/-/tinyspy-2.2.0.tgz#9dc04b072746520b432f77ea2c2d17933de5d6ce"
@@ -30855,6 +30981,11 @@ type-fest@^1.0.2:
resolved "https://registry.npmmirror.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
+type-fest@^2.13.0:
+ version "2.19.0"
+ resolved "https://registry.npmmirror.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
+ integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
+
type-fest@^3.0.0:
version "3.13.1"
resolved "https://registry.npmmirror.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706"
@@ -31145,6 +31276,11 @@ undici-types@~6.20.0:
resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
+undici-types@~6.21.0:
+ version "6.21.0"
+ resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
+ integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
+
unescape@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/unescape/-/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96"
@@ -31406,10 +31542,10 @@ update-browserslist-db@^1.1.1:
escalade "^3.2.0"
picocolors "^1.1.0"
-update-check@1.5.2:
- version "1.5.2"
- resolved "https://registry.npmmirror.com/update-check/-/update-check-1.5.2.tgz#2fe09f725c543440b3d7dabe8971f2d5caaedc28"
- integrity sha512-1TrmYLuLj/5ZovwUS7fFd1jMH3NnFDN1y1A8dboedIDt7zs/zJMo6TwwlhYKkSeEwzleeiSBV5/3c9ufAQWDaQ==
+update-check@1.5.4:
+ version "1.5.4"
+ resolved "https://registry.npmmirror.com/update-check/-/update-check-1.5.4.tgz#5b508e259558f1ad7dbc8b4b0457d4c9d28c8743"
+ integrity sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==
dependencies:
registry-auth-token "3.3.2"
registry-url "3.1.0"
@@ -31634,7 +31770,7 @@ uuid@^10.0.0:
resolved "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
-uuid@^3.2.1, uuid@^3.3.2:
+uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
@@ -32189,12 +32325,12 @@ widest-line@^2.0.0:
dependencies:
string-width "^2.1.1"
-widest-line@^3.1.0:
- version "3.1.0"
- resolved "https://registry.npmmirror.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
- integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
+widest-line@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmmirror.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2"
+ integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==
dependencies:
- string-width "^4.0.0"
+ string-width "^5.0.1"
wildcard@^1.1.0:
version "1.1.2"
@@ -32390,7 +32526,7 @@ ws@^7.0.0:
resolved "https://registry.npmmirror.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
-ws@^7.3.1:
+ws@^7.3.1, ws@~7.5.10:
version "7.5.10"
resolved "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
@@ -32400,11 +32536,6 @@ ws@^8.13.0, ws@^8.18.0:
resolved "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
-ws@~7.4.0:
- version "7.4.6"
- resolved "https://registry.npmmirror.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
- integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
-
ws@~8.17.1:
version "8.17.1"
resolved "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
@@ -32433,9 +32564,9 @@ xlsx@^0.17.0:
resolved "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz#0f64eeed3f1a46e64724620c3553f2dbd3cd2d7d"
xml-crypto@^3.0.1:
- version "3.2.0"
- resolved "https://registry.npmmirror.com/xml-crypto/-/xml-crypto-3.2.0.tgz#a9debab572c8e895cff5fb351a8d8be3f6e1962e"
- integrity sha512-qVurBUOQrmvlgmZqIVBqmb06TD2a/PpEUfFPgD7BuBfjmoH4zgkqaWSIJrnymlCvM2GGt9x+XtJFA+ttoAufqg==
+ version "3.2.1"
+ resolved "https://registry.npmmirror.com/xml-crypto/-/xml-crypto-3.2.1.tgz#df2511a95275b43e18924693ff932af7de3217a4"
+ integrity sha512-0GUNbPtQt+PLMsC5HoZRONX+K6NBJEqpXe/lsvrFj0EqfpGPpVfJKGE7a5jCg8s2+Wkrf/2U1G41kIH+zC9eyQ==
dependencies:
"@xmldom/xmldom" "^0.8.8"
xpath "0.0.32"
@@ -32557,6 +32688,11 @@ yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1:
resolved "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+yallist@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmmirror.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533"
+ integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==
+
yaml@2.3.1:
version "2.3.1"
resolved "https://registry.npmmirror.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
@@ -32572,14 +32708,6 @@ yaml@^2.2.2:
resolved "https://registry.npmmirror.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98"
integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==
-yamljs@0.3.0:
- version "0.3.0"
- resolved "https://registry.npmmirror.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b"
- integrity sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==
- dependencies:
- argparse "^1.0.7"
- glob "^7.0.5"
-
yargs-parser@13.1.2:
version "13.1.2"
resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
@@ -32752,11 +32880,16 @@ zip-stream@^5.0.1:
compress-commons "^5.0.1"
readable-stream "^3.6.0"
-zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.24.1:
+zod-to-json-schema@^3.22.3:
version "3.24.3"
resolved "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz#5958ba111d681f8d01c5b6b647425c9b8a6059e7"
integrity sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==
+zod-to-json-schema@^3.24.1:
+ version "3.24.5"
+ resolved "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3"
+ integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
+
zod@^3.22.4, zod@^3.24.1:
version "3.24.2"
resolved "https://registry.npmmirror.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"