Merge branch 'next' into develop

This commit is contained in:
nocobase[bot]
2026-05-13 07:17:44 +00:00
149 changed files with 2487 additions and 816 deletions
+2 -3
View File
@@ -168,8 +168,6 @@ const external = [
'ahooks',
'lodash',
'china-division',
'file-saver',
// langChain
'langchain',
'@langchain/core',
@@ -597,12 +595,13 @@ export async function buildPluginClient(
}
const sourcePackages = getPluginBrowserSourcePackages(sourceCwds, globExcludeFiles);
const excludePackages = getExcludePackages(sourcePackages, external, pluginPrefix);
const browserExternalPackages = excludePackages.filter((packageName) => packageName !== 'file-saver');
checkRequire(clientFiles, log);
buildCheck({ cwd, packageJson, entry: lane, files: clientFiles, log });
const outDir = path.join(cwd, target_dir, laneConfig.distDir);
const globals = excludePackages.reduce<Record<string, string>>((prev, curr) => {
const globals = browserExternalPackages.reduce<Record<string, string>>((prev, curr) => {
if (curr.startsWith('@nocobase')) {
laneConfig.externalSubpaths.forEach((subpath) => {
prev[`${curr}/${subpath}`] = `${curr}/${subpath}`;
+2
View File
@@ -29,9 +29,11 @@
"@nocobase/shared": "2.1.0-alpha.34",
"ahooks": "^3.7.2",
"antd": "5.24.2",
"antd-style": "3.7.1",
"axios": "^1.7.0",
"classnames": "^2.3.1",
"dayjs": "^1.11.10",
"file-saver": "^2.0.5",
"i18next": "^22.4.9",
"json5": "^2.2.3",
"lodash": "4.17.21",
@@ -59,7 +59,7 @@ export const titleField = defineAction({
};
},
defaultParams: (ctx: any) => {
const titleField = ctx.model.context.collectionField.targetCollectionTitleFieldName;
const titleField = ctx.model?.context?.collectionField?.targetCollectionTitleFieldName;
return {
label: ctx.model.parent?.props?.titleField || ctx.model.props.titleField || titleField,
};
@@ -18,7 +18,7 @@ export const validation = defineAction({
return;
}
const targetInterface = ctx.model.collectionField.getInterfaceOptions();
if (!targetInterface.validationType) {
if (!targetInterface?.validationType) {
return null;
}
return {
@@ -431,7 +431,7 @@ FormItemModel.registerFlow({
},
defaultParams: (ctx: any) => {
const titleField =
ctx.model.props.titleField || ctx.model.context.collectionField.targetCollectionTitleFieldName;
ctx.model.props.titleField || ctx.model?.context?.collectionField?.targetCollectionTitleFieldName;
return {
titleField: titleField,
};
@@ -541,7 +541,7 @@ TableColumnModel.registerFlow({
ctx.model.setProps(targetCollectionField.getComponentProps());
},
defaultParams: (ctx: any) => {
const titleField = ctx.model.context.collectionField.targetCollectionTitleFieldName;
const titleField = ctx.model?.context?.collectionField?.targetCollectionTitleFieldName;
return {
label: getSavedAssociationTitleField(ctx.model) || titleField,
};
@@ -9,7 +9,9 @@
import * as antdCssinjs from '@ant-design/cssinjs';
import * as antdIcons from '@ant-design/icons';
import * as antdStyle from 'antd-style';
import * as emotionCss from '@emotion/css';
import * as formilyAntdV5 from '@formily/antd-v5';
import * as formilyCore from '@formily/core';
import * as formilyReact from '@formily/react';
import * as formilyReactive from '@formily/reactive';
@@ -19,6 +21,7 @@ import { dayjs } from '@nocobase/utils/client';
import * as nocobaseFlowEngine from '@nocobase/flow-engine';
import * as ahooks from 'ahooks';
import * as antd from 'antd';
import * as FileSaver from 'file-saver';
import axios from 'axios';
import * as i18next from 'i18next';
import lodash from 'lodash';
@@ -49,12 +52,14 @@ export function defineGlobalDeps(requirejs: RequireJS) {
requirejs.define('antd', () => antd);
requirejs.define('@ant-design/icons', () => antdIcons);
requirejs.define('@ant-design/cssinjs', () => antdCssinjs);
requirejs.define('antd-style', () => antdStyle);
// i18next
requirejs.define('i18next', () => i18next);
requirejs.define('react-i18next', () => reactI18next);
// formily
requirejs.define('@formily/antd-v5', () => formilyAntdV5);
requirejs.define('@formily/core', () => formilyCore);
requirejs.define('@formily/react', () => formilyReact);
requirejs.define('@formily/reactive', () => formilyReactive);
@@ -73,4 +78,5 @@ export function defineGlobalDeps(requirejs: RequireJS) {
requirejs.define('dayjs', () => dayjs);
requirejs.define('lodash', () => lodash);
requirejs.define('@emotion/css', () => emotionCss);
requirejs.define('file-saver', () => FileSaver);
}
@@ -0,0 +1,2 @@
export * from './dist/client-v2';
export { default } from './dist/client-v2';
@@ -0,0 +1 @@
module.exports = require('./dist/client-v2/index.js');
@@ -14,6 +14,7 @@
"license": "Apache-2.0",
"peerDependencies": {
"@nocobase/client": "2.x",
"@nocobase/client-v2": "2.x",
"@nocobase/server": "2.x",
"@nocobase/test": "2.x"
},
@@ -0,0 +1,99 @@
/**
* 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 { Select, Space } from 'antd';
import React, { useState, useEffect } from 'react';
import { css } from '@emotion/css';
import { BulkEditFormItemValueType } from '../types';
import { lang } from '../locale';
function toFormFieldValue(value: any) {
if (BulkEditFormItemValueType.Clear in value) {
return null;
} else if (BulkEditFormItemValueType.ChangedTo in value) {
return value[BulkEditFormItemValueType.ChangedTo];
} else if (BulkEditFormItemValueType.RemainsTheSame in value) {
return;
}
}
export const BulkEditFieldV2 = (props) => {
const { formItemModel, onChange, field, ...rest } = props;
const [type, setType] = useState<number>(BulkEditFormItemValueType.RemainsTheSame);
const [value, setValue] = useState(null);
const form = formItemModel.context.blockModel.form;
// 保存原始的校验规则
const [originalRules] = useState(() => formItemModel?.props?.rules || []);
const typeChangeHandler = (val) => {
setType(val);
const fieldValue = toFormFieldValue({ [val]: value });
form.setFieldValue(formItemModel.props.name, fieldValue);
if (val === BulkEditFormItemValueType.ChangedTo) {
const fieldUse = formItemModel?.subModels?.field?.use;
if (fieldUse === 'CheckboxFieldModel') {
// checkbox 默认值为 false
setTimeout(() => {
valueChangeHandler(false);
});
}
}
};
useEffect(() => {
const required = type === BulkEditFormItemValueType.ChangedTo;
// 设置必填状态
if (required) {
// 移除原始规则中的必填规则
const otherRules = originalRules.filter((rule) => !rule.required);
// 添加新的必填规则,保留其他原始规则
const rules = [
{
required: true,
message: formItemModel.context.t('The field value is required'),
},
...otherRules,
];
formItemModel?.setProps({ required: true, rules });
} else {
// 移除所有规则
formItemModel?.setProps({ required: false, rules: [] });
}
}, [type]);
const valueChangeHandler = (val) => {
const v = val?.target?.value ?? val?.target?.checked ?? val;
setValue(v);
onChange?.(v);
};
const child = React.cloneElement(field, {
...rest,
onChange: valueChangeHandler,
});
return (
<Space
className={css`
display: flex;
> .ant-space-item {
width: 100%;
}
`}
direction="vertical"
>
<Select defaultValue={type} value={type} onChange={typeChangeHandler} disabled={props.aclDisabled}>
<Select.Option value={BulkEditFormItemValueType.RemainsTheSame}>{lang('Remains the same')}</Select.Option>
<Select.Option value={BulkEditFormItemValueType.ChangedTo}>{lang('Changed to')}</Select.Option>
<Select.Option value={BulkEditFormItemValueType.Clear}>{lang('Clear')}</Select.Option>
</Select>
{[BulkEditFormItemValueType.ChangedTo, BulkEditFormItemValueType.AddAttach].includes(type) && child}
</Space>
);
};
@@ -8,9 +8,9 @@
*/
import { defineAction, tExpr } from '@nocobase/flow-engine';
import { DetailsItemModel } from '@nocobase/client';
import type { FieldModel } from '@nocobase/client';
import { getFieldBindingUse, rebuildFieldSubModel } from './rebuildFieldSubModel';
import { DetailsItemModel } from '@nocobase/client-v2';
import type { FieldModel } from '@nocobase/client-v2';
import { getFieldBindingUse, rebuildFieldSubModel } from '../rebuildFieldSubModel';
const bulkEditExcludedBindingModels = new Set([
'SubFormFieldModel',
@@ -8,7 +8,7 @@
*/
import { defineAction, DisplayItemModel } from '@nocobase/flow-engine';
import { titleField } from '@nocobase/client';
import { titleField } from '@nocobase/client-v2';
// 重新实现 RecordSelectFieldModel 中 titleField 的 bulkEditTitleField 版本
export const bulkEditTitleField = defineAction({
@@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ActionSceneEnum, PopupActionModel } from '@nocobase/client';
import { ActionSceneEnum, PopupActionModel } from '@nocobase/client-v2';
import { tExpr } from '@nocobase/flow-engine';
import type { ButtonProps } from 'antd/es/button';
import { NAMESPACE, lang } from '../locale';
import { createTagPageOptions } from './utils';
import { NAMESPACE, lang } from '../../locale';
import { createTagPageOptions } from '../utils';
const SETTINGS_FLOW_KEY = 'bulkEditSettings';
@@ -7,12 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ChildPageTabModel, BlockGridModel, SkeletonFallback } from '@nocobase/client';
import { ChildPageTabModel, BlockGridModel, SkeletonFallback } from '@nocobase/client-v2';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { useRequest } from 'ahooks';
import React from 'react';
import { BlockSceneEnum } from '@nocobase/client';
import { BlockSceneEnum } from '@nocobase/client-v2';
function PageTabChildrenRenderer({ ctx, options }) {
const { data, loading } = useRequest(
@@ -8,7 +8,7 @@
*/
import { buildSubModelItems, DefaultStructure, tExpr } from '@nocobase/flow-engine';
import _ from 'lodash';
import { DataBlockModel, BlockModel } from '@nocobase/client';
import { DataBlockModel, BlockModel } from '@nocobase/client-v2';
export class BulkEditDataBlockModel<T = DefaultStructure> extends DataBlockModel<T> {}
@@ -7,14 +7,14 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { FieldModelRenderer, FieldModel, RecordSelectFieldModel, titleField } from '@nocobase/client';
import { FieldModel, RecordSelectFieldModel, titleField } from '@nocobase/client-v2';
import { ParamObject, StepParams, FieldModelRenderer } from '@nocobase/flow-engine';
import { Select, Space } from 'antd';
import React, { useEffect, useState } from 'react';
import { css } from '@emotion/css';
import _ from 'lodash';
import { BulkEditFormItemValueType } from './types';
import { lang } from '../locale';
import { ParamObject, StepParams } from '@nocobase/flow-engine';
import { BulkEditFormItemValueType } from '../../types';
import { lang } from '../../locale';
import { BulkEditFormItemModel } from './BulkEditFormItemModel';
function toFormFieldValue(value: any) {
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ActionGroupModel } from '@nocobase/client';
import { ActionGroupModel } from '@nocobase/client-v2';
import { BulkEditFormSubmitActionModel } from './BulkEditFormSubmitActionModel';
export class BulkEditFormActionGroupModel extends ActionGroupModel {}
@@ -7,11 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { FormGridModel } from '@nocobase/client';
import { FormGridModel } from '@nocobase/client-v2';
import { SettingOutlined } from '@ant-design/icons';
import { AddSubModelButton, FlowSettingsButton } from '@nocobase/flow-engine';
import { AddSubModelButton, FlowSettingsButton, tExpr } from '@nocobase/flow-engine';
import React from 'react';
import { tExpr } from '@nocobase/flow-engine';
export class BulkEditFormGridModel extends FormGridModel {
// 显示字段的设置项
@@ -8,10 +8,9 @@
*/
import React from 'react';
import { FormItemModel, FieldModel } from '@nocobase/client';
import { FormItemModel, FieldModel } from '@nocobase/client-v2';
import _ from 'lodash';
import { BulkEditFieldV2 } from '../component/BulkEditFieldV2';
import { BulkEditFieldV2 } from '../../component/BulkEditFieldV2';
import { tExpr, FieldModelRenderer, FormItem } from '@nocobase/flow-engine';
function buildDynamicName(nameParts: string[], fieldIndex: string[]) {
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CreateFormModel, BlockSceneEnum } from '@nocobase/client';
import { CreateFormModel, BlockSceneEnum } from '@nocobase/client-v2';
import { tExpr } from '@nocobase/flow-engine';
@@ -7,12 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { FormSubmitActionModel } from '@nocobase/client';
import { FormSubmitActionModel } from '@nocobase/client-v2';
import { ButtonProps } from 'antd';
import { tExpr } from '@nocobase/flow-engine';
import { submitHandler } from './submitHandler';
import { submitHandler } from '../../submitHandler';
import { BulkEditFormModel } from './BulkEditFormModel';
import { lang } from '../locale';
import { lang } from '../../locale';
export class BulkEditFormSubmitActionModel extends FormSubmitActionModel {
defaultProps: ButtonProps & { withScheduleSend?: boolean } = {
@@ -1,12 +1,3 @@
/**
* 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 './BulkEditActionModel';
export * from './BulkEditFormModel';
export * from './BulkEditFormItemModel';
@@ -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.
*/
import { Plugin } from '@nocobase/client-v2';
import { bulkEditTitleField } from './flow/bulkEditTitleField';
import { bulkEditFieldComponent } from './flow/bulkEditFieldComponent';
import * as models from './flow/models';
export class PluginActionBulkEditClient extends Plugin {
async load() {
// 先注册 bulkEditTitleField/bulkEditFieldComponent action,再导入 models
this.app.flowEngine.registerActions({ bulkEditTitleField, bulkEditFieldComponent });
this.app.flowEngine.registerModels(models);
}
}
export default PluginActionBulkEditClient;
@@ -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 { useTranslation } from 'react-i18next';
export const NAMESPACE = 'action-bulk-edit';
export function lang(key: string) {
const runtimeI18n = (globalThis as any)?.window?.__nocobase_i18n__;
return runtimeI18n?.t?.(key, { ns: NAMESPACE }) ?? key;
}
export function generateNTemplate(key: string) {
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
}
export function useBulkEditTranslation() {
return useTranslation(NAMESPACE, {
nsMode: 'fallback',
});
}
@@ -14,7 +14,7 @@
* - pattern
* - beforeRenderuseCache: false
*/
import { FieldModel } from '@nocobase/client';
import { FieldModel } from '@nocobase/client-v2';
import { FlowEngine, FlowModel, StepParams } from '@nocobase/flow-engine';
type FieldBindingParams = {
@@ -6,8 +6,8 @@
* 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 type { FormBlockModel } from '@nocobase/client';
import { lang } from '../locale';
import type { FormBlockModel } from '@nocobase/client-v2';
import { lang } from './locale';
export async function submitHandler(ctx, params) {
const blockModel = ctx.blockModel as FormBlockModel;
@@ -10,7 +10,7 @@
import { Select, Space } from 'antd';
import React, { useState, useEffect } from 'react';
import { css } from '@emotion/css';
import { BulkEditFormItemValueType } from '../models/types';
import { BulkEditFormItemValueType } from '../../client-v2/types';
import { lang } from '../locale';
function toFormFieldValue(value: any) {
@@ -8,37 +8,38 @@
*/
import { Plugin, useActionAvailable } from '@nocobase/client';
import {
bulkEditActionSettings,
deprecatedBulkEditActionSettings,
bulkEditFormSubmitActionSettings,
} from './BulkEditAction.Settings';
import * as BulkEditActionSettings from './BulkEditAction.Settings';
import { BulkEditActionDecorator } from './BulkEditActionDecorator';
import { BulkEditActionInitializer } from './BulkEditActionInitializer';
import {
BulkEditBlockInitializers_deprecated,
CreateFormBulkEditBlockInitializers,
bulkEditBlockInitializers,
} from './BulkEditBlockInitializers';
import {
BulkEditFormActionInitializers_deprecated,
bulkEditFormActionInitializers,
} from './BulkEditFormActionInitializers';
import { BulkEditFormItemInitializers_deprecated, bulkEditFormItemInitializers } from './BulkEditFormItemInitializers';
import * as BulkEditBlockInitializers from './BulkEditBlockInitializers';
import * as BulkEditFormActionInitializers from './BulkEditFormActionInitializers';
import * as BulkEditFormItemInitializers from './BulkEditFormItemInitializers';
import { bulkEditFormItemSettings } from './bulkEditFormItemSettings';
import { bulkEditFormBlockSettings } from './BulkEditFormBlockSettings';
import { BulkEditField } from './component/BulkEditField';
import { useCustomizeBulkEditActionProps } from './utils';
import { bulkEditTitleField } from './models/bulkEditTitleField';
import { bulkEditFieldComponent } from './models/bulkEditFieldComponent';
import * as models from './models';
import { bulkEditTitleField } from '../client-v2/flow/bulkEditTitleField';
import { bulkEditFieldComponent } from '../client-v2/flow/bulkEditFieldComponent';
import * as models from '../client-v2/flow/models';
const deprecatedBulkEditActionSettings = (BulkEditActionSettings as any).deprecatedBulkEditActionSettings;
const BulkEditBlockInitializers_deprecated = (BulkEditBlockInitializers as any).BulkEditBlockInitializers_deprecated;
const CreateFormBulkEditBlockInitializers = (BulkEditBlockInitializers as any).CreateFormBulkEditBlockInitializers;
const BulkEditFormActionInitializers_deprecated = (BulkEditFormActionInitializers as any)
.BulkEditFormActionInitializers_deprecated;
const BulkEditFormItemInitializers_deprecated = (BulkEditFormItemInitializers as any)
.BulkEditFormItemInitializers_deprecated;
const { bulkEditActionSettings, bulkEditFormSubmitActionSettings } = BulkEditActionSettings;
const { bulkEditBlockInitializers } = BulkEditBlockInitializers;
const { bulkEditFormActionInitializers } = BulkEditFormActionInitializers;
const { bulkEditFormItemInitializers } = BulkEditFormItemInitializers;
export class PluginActionBulkEditClient extends Plugin {
async load() {
// 先注册 bulkEditTitleField/bulkEditFieldComponent action,再导入 models
this.app.flowEngine.registerActions({ bulkEditTitleField, bulkEditFieldComponent });
this.app.flowEngine.registerModels(models);
this.app.addComponents({ BulkEditField, BulkEditActionDecorator });
this.app.addScopes({ useCustomizeBulkEditActionProps });
this.app.schemaSettingsManager.add(bulkEditFormBlockSettings);
@@ -0,0 +1,2 @@
export * from './dist/client-v2';
export { default } from './dist/client-v2';
@@ -0,0 +1 @@
module.exports = require('./dist/client-v2/index.js');
@@ -21,6 +21,7 @@
},
"peerDependencies": {
"@nocobase/client": "2.x",
"@nocobase/client-v2": "2.x",
"@nocobase/server": "2.x",
"@nocobase/test": "2.x"
},
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ActionModel, ActionSceneEnum, AssignFormModel } from '@nocobase/client';
import { ActionModel, ActionSceneEnum, AssignFormModel } from '@nocobase/client-v2';
import {
FlowModelRenderer,
resolveRunJSObjectValues,
@@ -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.
*/
import { Plugin } from '@nocobase/client-v2';
import { BulkUpdateActionModel } from './BulkUpdateActionModel';
export class PluginActionBulkUpdateClient extends Plugin {
async load() {
// 注册 Flow 模型以支持新版流程引擎按钮动作
this.app.flowEngine.registerModels({
BulkUpdateActionModel,
});
}
}
export { BulkUpdateActionModel } from './BulkUpdateActionModel';
export default PluginActionBulkUpdateClient;
@@ -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 const NAMESPACE = 'action-bulk-update';
@@ -9,7 +9,7 @@
import { Plugin, useActionAvailable } from '@nocobase/client';
import { bulkUpdateActionSettings, deprecatedBulkUpdateActionSettings } from './BulkUpdateAction.Settings';
import { BulkUpdateActionModel } from './BulkUpdateActionModel';
import { BulkUpdateActionModel } from '../client-v2/BulkUpdateActionModel';
import { BulkUpdateActionInitializer } from './BulkUpdateActionInitializer';
import { CustomizeActionInitializer } from './CustomizeActionInitializer';
import { useCustomizeBulkUpdateActionProps } from './utils';
@@ -36,5 +36,5 @@ export class PluginActionBulkUpdateClient extends Plugin {
this.app.schemaInitializerManager.addItem('map:configureActions', 'customize.bulkUpdate', initializerData);
}
}
export { BulkUpdateActionModel } from './BulkUpdateActionModel';
export { BulkUpdateActionModel } from '../client-v2/BulkUpdateActionModel';
export default PluginActionBulkUpdateClient;
@@ -9,8 +9,7 @@
import { i18n } from '@nocobase/client';
import { useTranslation } from 'react-i18next';
export const NAMESPACE = 'action-bulk-update';
import { NAMESPACE } from '../../client-v2/locale';
export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE });
@@ -0,0 +1,2 @@
export * from './dist/client-v2';
export { default } from './dist/client-v2';
@@ -0,0 +1 @@
module.exports = require('./dist/client-v2/index.js');
@@ -16,18 +16,21 @@
"@formily/react": "2.x",
"@formily/shared": "2.x",
"antd": "5.x",
"file-saver": "^2.0.5",
"lodash": "4.x",
"react-i18next": "^11.15.1",
"react-router-dom": "6.x"
},
"nocobase": {
"supportedVersions": [
"1.x"
"1.x",
"2.x"
],
"editionLevel": 0
},
"peerDependencies": {
"@nocobase/client": "2.x",
"@nocobase/client-v2": "2.x",
"@nocobase/server": "2.x",
"@nocobase/test": "2.x"
},
@@ -7,21 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import {
ActionSceneEnum,
CollectionActionGroupModel,
FormActionGroupModel,
RecordActionGroupModel,
ActionModel,
PopupSubTableFormActionGroupModel,
} from '@nocobase/client';
import { ActionModel, ActionSceneEnum } from '@nocobase/client-v2';
import { tExpr } from '@nocobase/flow-engine';
import type { ButtonProps } from 'antd/es/button';
import { NAMESPACE } from '../locale';
import { NAMESPACE } from './locale';
import { CUSTOM_REQUEST_ACTION_NAME } from './customRequestFlowAction';
export class CustomRequestActionModel extends ActionModel {
static scene = ActionSceneEnum.all;
defaultProps: ButtonProps = {
title: tExpr('Custom request', { ns: NAMESPACE }),
};
@@ -54,18 +48,15 @@ CustomRequestActionModel.registerFlow({
},
});
CollectionActionGroupModel.registerActionModels({
CustomRequestActionModel,
});
RecordActionGroupModel.registerActionModels({
CustomRequestActionModel,
});
FormActionGroupModel.registerActionModels({
CustomRequestActionModel,
});
PopupSubTableFormActionGroupModel.registerActionModels({
CustomRequestActionModel,
});
export function registerCustomRequestActionGroups(flowEngine: any) {
[
'CollectionActionGroupModel',
'RecordActionGroupModel',
'FormActionGroupModel',
'PopupSubTableFormActionGroupModel',
].forEach((modelName) => {
flowEngine.getModelClass(modelName)?.registerActionModels?.({
CustomRequestActionModel,
});
});
}
@@ -11,7 +11,11 @@ import { useForm } from '@formily/react';
import { useFlowContext } from '@nocobase/flow-engine';
import { Input } from 'antd';
import React from 'react';
import { loadCustomRequestRecord, makeRequestKey, toCustomRequestConfigurationInitialValues } from '../utils';
import {
loadCustomRequestRecord,
makeRequestKey,
toCustomRequestConfigurationInitialValues,
} from '../customRequestUtils';
const requestConfigInFlight = new Map<string, Promise<any>>();
@@ -62,7 +66,7 @@ export const RequestKeyField = (props: { value?: string; onChange?: (value: stri
});
loadedKeyRef.current = value;
} catch (error) {
// ignore - this key may not have existing config yet
console.log(error);
}
})();
@@ -0,0 +1,66 @@
/**
* 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 { useFlowContext } from '@nocobase/flow-engine';
import { Select } from 'antd';
import React from 'react';
import { normalizeRoleNames } from '../customRequestUtils';
type RoleOption = {
label: string;
value: string;
};
export const RolesSelect = (props: { value?: string[]; onChange?: (value: string[]) => void }) => {
const { value = [], onChange } = props;
const { api } = useFlowContext();
const [options, setOptions] = React.useState<RoleOption[]>([]);
React.useEffect(() => {
let mounted = true;
(async () => {
try {
const res = await api.resource('roles').list({
pageSize: 200,
sort: ['createdAt'],
});
if (!mounted) {
return;
}
const data = Array.isArray(res?.data?.data) ? res.data.data : [];
setOptions(
data
.map((item: any) => ({
label: item?.title || item?.name,
value: item?.name,
}))
.filter((item: RoleOption) => !!item.value),
);
} catch (error) {
if (mounted) {
setOptions([]);
}
}
})();
return () => {
mounted = false;
};
}, [api]);
return (
<Select
mode="multiple"
allowClear
options={options}
value={value}
onChange={(next) => onChange?.(normalizeRoleNames(next))}
/>
);
};
@@ -8,15 +8,15 @@
*/
import { ActionScene, defineAction, tExpr } from '@nocobase/flow-engine';
import { NAMESPACE } from '../locale';
import { executeCustomRequest } from './utils';
import { NAMESPACE } from './locale';
import { CustomRequestStepParams } from './customRequestFlowActionTypes';
import {
DEFAULT_CUSTOM_REQUEST_SETTINGS,
executeCustomRequest,
extractVariablePaths,
makeRequestKey,
saveCustomRequestConfig,
} from './utils';
} from './customRequestUtils';
import { customRequestUiSchema } from './customRequestUiSchema';
const buildSanitizedParams = (key: string, params: CustomRequestStepParams): CustomRequestStepParams => {
@@ -7,11 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { RemoteSelect, TextAreaWithContextSelector } from '@nocobase/client';
import { TextAreaWithContextSelector } from '@nocobase/client-v2';
import { css } from '@emotion/css';
import { InputNumber } from 'antd';
import { generateNTemplate } from '../locale';
import { FlowJsonWithContextSelector } from './components/FlowJsonWithContextSelector';
import { generateNTemplate } from './locale';
import { RequestKeyField } from './components/RequestKeyField';
import { RolesSelect } from './components/RolesSelect';
@@ -108,6 +107,7 @@ export const customRequestUiSchema = {
'x-component': TextAreaWithContextSelector,
'x-component-props': {
rows: 1,
maxRows: 4,
},
},
remove: {
@@ -173,6 +173,7 @@ export const customRequestUiSchema = {
'x-component': TextAreaWithContextSelector,
'x-component-props': {
rows: 1,
maxRows: 4,
},
},
remove: {
@@ -196,11 +197,10 @@ export const customRequestUiSchema = {
type: 'string',
title: generateNTemplate('Body'),
'x-decorator': 'FormItem',
'x-component': FlowJsonWithContextSelector,
'x-component': TextAreaWithContextSelector,
'x-component-props': {
autoSize: {
minRows: 10,
},
rows: 10,
maxRows: 20,
placeholder: generateNTemplate('Input request data'),
},
description: generateNTemplate('Only support standard JSON data'),
@@ -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.
*/
import { Plugin } from '@nocobase/client-v2';
import { CustomRequestActionModel, registerCustomRequestActionGroups } from './CustomRequestActionModel';
import { customRequestFlowAction } from './customRequestFlowAction';
export class PluginActionCustomRequestClient extends Plugin {
async load() {
this.app.flowEngine.registerActions({ customRequestFlowAction });
this.app.flowEngine.registerModels({
CustomRequestActionModel,
});
registerCustomRequestActionGroups(this.app.flowEngine);
}
}
export { CustomRequestActionModel } from './CustomRequestActionModel';
export { customRequestFlowAction } from './customRequestFlowAction';
export default PluginActionCustomRequestClient;
@@ -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 { useTranslation as useT } from 'react-i18next';
export const NAMESPACE = 'action-custom-request';
export function lang(key: string) {
const runtimeI18n = (globalThis as any)?.window?.__nocobase_i18n__;
return runtimeI18n?.t?.(key, { ns: NAMESPACE }) ?? key;
}
export function generateNTemplate(key: string) {
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
}
export function useTranslation() {
return useT(NAMESPACE, {
nsMode: 'fallback',
});
}
@@ -9,13 +9,13 @@
import { Plugin, SchemaComponentOptions } from '@nocobase/client';
import React from 'react';
import { CustomRequestActionModel, registerCustomRequestActionGroups } from '../client-v2/CustomRequestActionModel';
import { customRequestFlowAction } from '../client-v2/customRequestFlowAction';
import { CustomRequestAction } from './components';
import { customRequestActionSettings } from './components/CustomRequestActionDesigner';
import { CustomRequestInitializer } from './initializer';
import { customRequestFlowAction } from './models/customRequestFlowAction';
import { customizeCustomRequestActionSettings } from './schemaSettings';
import { CustomRequestConfigurationFieldsSchema } from './schemas';
import { CustomRequestActionModel } from './models/CustomRequestActionModel';
const CustomRequestProvider: React.FC = (props) => {
return (
@@ -37,6 +37,7 @@ export class PluginActionCustomRequestClient extends Plugin {
this.app.flowEngine.registerModels({
CustomRequestActionModel,
});
registerCustomRequestActionGroups(this.app.flowEngine);
this.app.schemaSettingsManager.add(customizeCustomRequestActionSettings);
// @deprecated
@@ -9,8 +9,9 @@
import { i18n } from '@nocobase/client';
import { useTranslation as useT } from 'react-i18next';
import { NAMESPACE } from '../client-v2/locale';
export const NAMESPACE = 'action-custom-request';
export { NAMESPACE };
export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE });
@@ -1,106 +0,0 @@
/**
* 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 { css } from '@emotion/css';
import { FlowContextSelector, useFlowContext } from '@nocobase/flow-engine';
import { Input } from '@nocobase/client';
import { Button } from 'antd';
import type { TextAreaProps } from 'antd/es/input';
import React, { useCallback, useMemo, useRef } from 'react';
const JsonInput = Input.JSON as any;
export type FlowJsonWithContextSelectorProps = TextAreaProps & {
value?: any;
onChange?: (value: any) => void;
};
function setNativeInputValue(input: HTMLTextAreaElement, value: string) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(input.constructor.prototype, 'value')?.set;
nativeInputValueSetter?.call(input, value);
input.dispatchEvent(
new Event('input', {
bubbles: true,
}),
);
}
export const FlowJsonWithContextSelector: React.FC<FlowJsonWithContextSelectorProps> = ({
value,
onChange,
...props
}) => {
const flowCtx = useFlowContext();
const inputRef = useRef<any>(null);
const insertAtCaret = useCallback((toInsert: string) => {
const textArea = inputRef.current?.resizableTextArea?.textArea as HTMLTextAreaElement | undefined;
if (!textArea) {
return;
}
const currentValue = textArea.value || '';
const start = textArea.selectionStart ?? currentValue.length;
const end = textArea.selectionEnd ?? start;
const nextValue = currentValue.slice(0, start) + toInsert + currentValue.slice(end);
setNativeInputValue(textArea, nextValue);
requestAnimationFrame(() => {
const pos = start + toInsert.length;
textArea.setSelectionRange(pos, pos);
textArea.focus();
});
}, []);
const handleVariableSelected = useCallback(
(varValue: string) => {
if (!varValue) {
return;
}
insertAtCaret(varValue);
},
[insertAtCaret],
);
const metaTree = useMemo(() => () => flowCtx.getPropertyMetaTree?.(), [flowCtx]);
return (
<div style={{ position: 'relative', width: '100%' }}>
<JsonInput ref={inputRef} value={value} onChange={onChange} {...props} />
<div
style={{
position: 'absolute',
top: 0,
right: 0,
zIndex: 1,
lineHeight: 0,
}}
>
<FlowContextSelector metaTree={metaTree} onChange={(val) => handleVariableSelected(val)}>
<Button
type="default"
style={{ fontStyle: 'italic', fontFamily: 'New York, Times New Roman, Times, serif' }}
className={css`
&:not(:hover) {
border-right-color: transparent;
border-top-color: transparent;
background-color: transparent;
}
`}
>
x
</Button>
</FlowContextSelector>
</div>
</div>
);
};
export default FlowJsonWithContextSelector;
@@ -1,33 +0,0 @@
/**
* 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 { RemoteSelect } from '@nocobase/client';
import React from 'react';
import { normalizeRoleNames } from '../utils';
export const RolesSelect = (props: { value?: string[]; onChange?: (value: string[]) => void }) => {
const { value = [], onChange } = props;
return (
<RemoteSelect
mode="multiple"
manual={false}
value={value}
service={{
resource: 'roles',
}}
fieldNames={{
label: 'title',
value: 'name',
}}
allowClear
onChange={(next) => onChange?.(normalizeRoleNames(next))}
/>
);
};
@@ -0,0 +1,2 @@
export * from './dist/client-v2';
export { default } from './dist/client-v2';
@@ -0,0 +1 @@
module.exports = require('./dist/client-v2/index.js');
@@ -24,6 +24,7 @@
},
"peerDependencies": {
"@nocobase/client": "2.x",
"@nocobase/client-v2": "2.x",
"@nocobase/server": "2.x",
"@nocobase/test": "2.x"
},
@@ -8,20 +8,21 @@
*/
import {
FlowModelRenderer,
MultiRecordResource,
tExpr,
useFlowContext,
useFlowViewContext,
FlowModelRenderer,
MultiRecordResource,
} from '@nocobase/flow-engine';
import { onFieldInputValueChange } from '@formily/core';
import { connect, mapProps, useForm } from '@formily/react';
import { ActionModel, ActionSceneEnum, useRequest, SkeletonFallback } from '@nocobase/client';
import { ActionModel, ActionSceneEnum, RecordActionGroupModel, SkeletonFallback } from '@nocobase/client-v2';
import { useRequest } from 'ahooks';
import { Tree as AntdTree } from 'antd';
import { cloneDeep } from 'lodash';
import React, { useEffect, useState } from 'react';
import { NAMESPACE } from './locale';
import { getCollectionState, getSyncFromForm } from './utils';
import { NAMESPACE } from '../locale';
const Tree = connect(
AntdTree,
@@ -77,7 +78,14 @@ function RemoteModelRenderer({ options }) {
const ctx = useFlowViewContext();
const { data, loading }: any = useRequest(
async () => {
const model: any = await ctx.engine.loadOrCreateModel(options, { delegateToParent: false, delegate: ctx });
const model: any = await ctx.engine.loadOrCreateModel(options, {
delegateToParent: false,
delegate: ctx,
skipSave: !ctx.flowSettingsEnabled,
});
if (options?.scene) {
model.actionName = options.scene;
}
return model;
},
{
@@ -89,8 +97,10 @@ function RemoteModelRenderer({ options }) {
}
return <FlowModelRenderer model={data} fallback={<SkeletonFallback style={{ margin: 16 }} />} />;
}
const EditDuplicateSubKey = 'deplicate.edit-form-grid-block';
function EditFormContent({ model }) {
function EditFormContent({ model, scene = 'create' }) {
const ctx = useFlowContext();
const { Header, type } = ctx.view;
model._closeView = ctx.view.close;
@@ -122,6 +132,7 @@ function EditFormContent({ model }) {
delegateToParent: false,
subType: 'object',
use: 'BlockGridModel',
scene,
}}
/>
</div>
@@ -142,6 +153,7 @@ export class DuplicateActionModel extends ActionModel {
getAclActionName() {
return 'create';
}
onClick(event) {
if (this.props.duplicateMode === 'quickDulicate') {
this.dispatchEvent(
@@ -167,6 +179,7 @@ export class DuplicateActionModel extends ActionModel {
);
}
}
getTitle() {
return this.duplicateLoading ? this.context.t('Duplicating') : this.props.title || this.context.t('Duplicate');
}
@@ -177,7 +190,6 @@ DuplicateActionModel.define({
sort: 60,
});
//复制模式
DuplicateActionModel.registerFlow({
key: 'duplicateModeSettings',
steps: {
@@ -306,7 +318,6 @@ DuplicateActionModel.registerFlow({
</a>
);
},
'x-reactions': [
{
dependencies: ['.duplicateMode'],
@@ -408,7 +419,6 @@ DuplicateActionModel.registerFlow({
margin: '2px 0',
},
},
'x-reactions': [
{
dependencies: ['.collection'],
@@ -458,7 +468,6 @@ async function fetchTemplateData(resource: any, template: { dataId: number; fiel
return res?.data;
}
//快捷创建
DuplicateActionModel.registerFlow({
key: 'duplicateSettings',
title: tExpr('Duplicate mode settings'),
@@ -504,7 +513,7 @@ DuplicateActionModel.registerFlow({
default: true,
fields:
duplicateFields?.filter((v) => {
return [...ctx.collection.fields.values()].find((k) => v.includes(k.name));
return typeof v === 'string' && [...ctx.collection.fields.values()].find((k) => v.includes(k.name));
}) || [],
};
const resource = ctx.createResource(MultiRecordResource);
@@ -527,7 +536,6 @@ DuplicateActionModel.registerFlow({
},
});
// 复制并继续填写
DuplicateActionModel.registerFlow({
key: 'popupSettings',
on: {
@@ -559,7 +567,7 @@ DuplicateActionModel.registerFlow({
size: 'medium',
},
async handler(ctx, params) {
await ctx.model.dispatchEvent('beforeRender'); // 确保 duplicateFields 已经分析过了
await ctx.model.dispatchEvent('beforeRender');
const { duplicateFields = [] } = ctx.model.props;
if (!duplicateFields?.length) {
ctx.message.error(
@@ -596,7 +604,7 @@ DuplicateActionModel.registerFlow({
default: true,
fields:
duplicateFields?.filter((v) => {
return [...ctx.collection.fields.values()].find((k) => v.includes(k.name));
return typeof v === 'string' && [...ctx.collection.fields.values()].find((k) => v.includes(k.name));
}) || [],
};
const resource = ctx.createResource(MultiRecordResource);
@@ -614,8 +622,6 @@ DuplicateActionModel.registerFlow({
if (shouldDelegateToOpenView) {
const delegatedParams: any = {
...(params || {}),
// DuplicateAction 的弹窗是自定义打开逻辑(openDuplicatePopup),
// 不能走 openView 动作默认的路由导航(否则路由会触发 model.click,而该模型并不监听 click)。
navigation: false,
scene: 'new',
formData,
@@ -644,7 +650,7 @@ DuplicateActionModel.registerFlow({
formData,
viewUid: runtimeViewUid,
},
content: () => <EditFormContent model={ctx.model} />,
content: () => <EditFormContent model={ctx.model} scene="create" />,
styles: {
content: {
padding: 0,
@@ -660,3 +666,7 @@ DuplicateActionModel.registerFlow({
},
},
});
RecordActionGroupModel.registerActionModels({
DuplicateActionModel,
});
@@ -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.
*/
import { Plugin } from '@nocobase/client-v2';
import { DuplicateActionModel } from './DuplicateActionModel';
export class PluginActionDuplicateClient extends Plugin {
async load() {
this.app.flowEngine.registerModels({
DuplicateActionModel,
});
}
}
export { DuplicateActionModel } from './DuplicateActionModel';
export default PluginActionDuplicateClient;
@@ -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 const NAMESPACE = 'action-duplicate';
@@ -9,7 +9,6 @@
import React from 'react';
import { Tag } from 'antd';
import { ArrayBase } from '@formily/antd-v5';
import { LRUCache } from 'lru-cache';
const TreeNode = (props) => {
@@ -33,9 +32,7 @@ const TreeNode = (props) => {
);
};
// 过滤掉系统字段
const systemKeys = [
// 'id',
'sort',
'createdById',
'createdBy',
@@ -47,8 +44,6 @@ const systemKeys = [
'sequence',
];
// 广度优先遍历查找一个 key 相等的节点并返回
// 注意:返回的 node 是一个响应式对象,修改它的属性会触发页面更新
function findNode(treeData, item) {
const queue = [...treeData];
while (queue.length) {
@@ -65,7 +60,6 @@ function findNode(treeData, item) {
function loadChildren({ node, traverseAssociations, traverseFields, systemKeys, dataSource, setDataSource }) {
const activeNode = findNode(dataSource, node);
let children = [];
// 多对多和多对一只展示关系字段
if (['belongsTo', 'belongsToMany'].includes(node.field.type) && node?.type === 'reference') {
children = traverseAssociations(node.field.target, {
exclude: systemKeys,
@@ -90,9 +84,6 @@ function loadChildren({ node, traverseAssociations, traverseFields, systemKeys,
}
export const getCollectionState = (dm, t, dataSourceKey, displayType = true) => {
/**
* maxDepth: 0 0 1
*/
const traverseFields = (collectionName, { exclude = [], depth = 0, maxDepth, prefix = '' }) => {
if (depth > maxDepth) {
return [];
@@ -120,7 +111,6 @@ export const getCollectionState = (dm, t, dataSourceKey, displayType = true) =>
isLeaf: true,
field,
};
// 多对多和多对一只展示关系字段
if (['belongsTo', 'belongsToMany'].includes(field.type)) {
node['type'] = 'reference';
option['type'] = 'reference';
@@ -150,8 +140,7 @@ export const getCollectionState = (dm, t, dataSourceKey, displayType = true) =>
if (depth > maxDepth || !dm.getCollection(dataSourceKey, collectionName)) {
return [];
}
// eslint-disable-next-line no-unsafe-optional-chaining
return [...dm.getCollection(dataSourceKey, collectionName)?.fields.values()]
return [...(dm.getCollection(dataSourceKey, collectionName)?.fields.values() || [])]
.map((field) => {
if (!field.target || !field.interface) {
return;
@@ -224,6 +213,7 @@ export const getSyncFromForm = (dm, t, dataSourceKey, collectionName, callBack)
if (depth > maxDepth) {
return [];
}
const safeFormData = Array.isArray(formData) ? formData : [];
const result = (
dm.getCollection(dataSourceKey, collectionName)
? [...dm.getCollection(dataSourceKey, collectionName).fields.values()]
@@ -252,7 +242,7 @@ export const getSyncFromForm = (dm, t, dataSourceKey, collectionName, callBack)
field,
disabled,
};
const targetFormField = formData.find((v) => v.name === option.key);
const targetFormField = safeFormData.find((v) => v?.name === option.key);
if (
['belongsTo', 'belongsToMany'].includes(field.type) &&
(!targetFormField || !targetFormField?.updateAssociation)
@@ -280,90 +270,66 @@ export const getSyncFromForm = (dm, t, dataSourceKey, collectionName, callBack)
depth: depth + 1,
maxDepth,
prefix: option.key,
exclude: ['id', ...systemKeys],
exclude: ['id', ...systemKeys.filter((v) => v !== 'sort')],
disabled: childrenDisabled,
},
formData,
safeFormData,
);
}
return option;
})
.filter(Boolean);
cache.set(collectionName + JSON.stringify(safeFormData), result);
return result;
};
})(new LRUCache<string, any>({ max: 100 }));
})(new LRUCache({ max: 50 }));
const traverseAssociations = ((cache) => {
return (collectionName, { prefix, maxDepth, depth = 0, exclude = [] }) => {
const cacheKey = `${collectionName}-${exclude.join(',')}-${depth}-${maxDepth}-${prefix}`;
const cachedResult = cache.get(cacheKey);
if (cachedResult) {
return cachedResult;
}
if (depth > maxDepth) {
return [];
}
const result = (
dm.getCollection(dataSourceKey, collectionName)
? [...dm.getCollection(dataSourceKey, collectionName).fields.values()]
: []
)
.map((field) => {
if (!field.target || !field.interface) {
return;
}
if (exclude.includes(field.name)) {
return;
}
const option = {
type: 'preloading',
tag: t(field.uiSchema?.title) || field.name,
};
const value = prefix ? `${prefix}.${field.name}` : field.name;
return {
type: 'preloading',
tag: t(field.uiSchema?.title) || field.name,
title: React.createElement(TreeNode, option),
key: value,
isLeaf: false,
field,
children: traverseAssociations(field.target, {
prefix: value,
depth: depth + 1,
maxDepth,
exclude,
}),
};
})
.filter(Boolean);
cache.set(cacheKey, result);
return result;
};
})(new LRUCache<string, any>({ max: 100 }));
const getEnableFieldTree = (collectionName: string, formData) => {
if (!collectionName) {
return [];
}
try {
return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 1, disabled: false }, formData);
} catch (error) {
console.error(error);
const traverseAssociations = (collectionName, { prefix, maxDepth, depth = 0, exclude = [] }) => {
if (depth > maxDepth || !dm.getCollection(dataSourceKey, collectionName)) {
return [];
}
return [...(dm.getCollection(dataSourceKey, collectionName)?.fields.values() || [])]
.map((field) => {
if (!field.target || !field.interface) {
return;
}
if (exclude.includes(field.name)) {
return;
}
const option = {
type: 'preloading',
tag: t(field.uiSchema?.title) || field.name,
};
const value = prefix ? `${prefix}.${field.name}` : field.name;
return {
title: React.createElement(TreeNode, option),
key: value,
disabled: true,
isLeaf: false,
field,
children: traverseAssociations(field.target, {
prefix: value,
depth: depth + 1,
maxDepth,
exclude,
}),
};
})
.filter(Boolean);
};
return {
async run(model) {
const formData = new Set([]);
const selectFields = new Set([]);
if (!model?.mapSubModels) {
callBack?.([], []);
return;
}
const getAssociationAppends = (model, subKey) => {
model.mapSubModels(subKey, (item) => {
const formData = new Set<any>();
const selectFields = new Set<string>();
const getAssociationAppends = (currentModel, subKey) => {
currentModel?.mapSubModels?.(subKey, (item) => {
if (
item.collectionField &&
!(['hasOne', 'hasMany'].includes(item.collectionField.type) || item.subModels.field?.updateAssociation)
@@ -376,26 +342,23 @@ export const getSyncFromForm = (dm, t, dataSourceKey, collectionName, callBack)
name: item.fieldPath,
updateAssociation: item.subModels.field?.updateAssociation,
});
if (item.subModels.field?.updateAssociation) {
if (item.subModels.field.subModels.grid) {
//子表单
getAssociationAppends(item.subModels.field.subModels.grid, 'items');
} else if (item.subModels.field.subModels.subTableColumns) {
//子表格(弹窗编辑)subTableColumns
getAssociationAppends(item.subModels.field, 'subTableColumns');
} else if (item.subModels.field.subModels.columns) {
//子表格(行内编辑)subTableColumns
getAssociationAppends(item.subModels.field, 'columns');
}
}
}
});
};
getAssociationAppends(model, 'items');
const treeData = getEnableFieldTree(collectionName, [...formData]);
if (callBack) {
callBack(treeData, [...selectFields]);
}
const fieldTree = traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 1 }, [...formData]);
callBack?.(fieldTree, [...selectFields]);
},
};
};
@@ -13,7 +13,7 @@ import { deprecatedDuplicateActionSettings, duplicateActionSettings } from './Du
import { DuplicateActionDecorator } from './DuplicateActionDecorator';
import { DuplicateActionInitializer } from './DuplicateActionInitializer';
import { DuplicatePluginProvider } from './DuplicatePluginProvider';
import { DuplicateActionModel } from './models/DuplicateActionModel';
import { DuplicateActionModel } from '../client-v2/DuplicateActionModel';
export class PluginActionDuplicateClient extends Plugin {
async load() {
@@ -51,3 +51,4 @@ export class PluginActionDuplicateClient extends Plugin {
export default PluginActionDuplicateClient;
export * from './DuplicateAction';
export { DuplicateActionModel } from '../client-v2/DuplicateActionModel';
@@ -9,8 +9,9 @@
import { i18n } from '@nocobase/client';
import { useTranslation } from 'react-i18next';
import { NAMESPACE } from '../../client-v2/locale';
export const NAMESPACE = 'action-duplicate';
export { NAMESPACE };
export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE });
@@ -0,0 +1,2 @@
export * from './dist/client-v2';
export { default } from './dist/client-v2';
@@ -0,0 +1 @@
module.exports = require('./dist/client-v2/index.js');
@@ -34,6 +34,7 @@
"peerDependencies": {
"@nocobase/actions": "2.x",
"@nocobase/client": "2.x",
"@nocobase/client-v2": "2.x",
"@nocobase/database": "2.x",
"@nocobase/server": "2.x",
"@nocobase/test": "2.x",
@@ -8,19 +8,21 @@
*/
import { escapeT } from '@nocobase/flow-engine';
import type { ButtonProps } from 'antd/es/button';
import { saveAs } from 'file-saver';
import { ActionModel, Cascader, ActionSceneEnum } from '@nocobase/client';
import { ActionModel, ActionSceneEnum } from '@nocobase/client-v2';
import { css } from '@emotion/css';
import { saveAs } from 'file-saver';
import { Cascader } from 'antd';
import type { ButtonProps } from 'antd/es/button';
import { getOptionFields } from './getOptionFields';
import { NAMESPACE } from '../locale';
import { NAMESPACE } from './locale';
const initExportSettings = (fields) => {
const exportSettings = fields
?.filter((f) => !f.isAssociationField() && f.options.interface)
.map((f) => ({ dataIndex: [f.name] }));
?.filter((field) => !field.isAssociationField() && field.options.interface)
.map((field) => ({ dataIndex: [field.name] }));
return exportSettings;
};
export class ExportActionModel extends ActionModel {
static scene = ActionSceneEnum.collection;
@@ -60,30 +62,28 @@ ExportActionModel.registerFlow({
const { title, fields, filterTargetKey } = currentBlock.collection;
const selectedRows = resource.getSelectedRows();
let filter;
// If there are selected rows, use the filter for selected rows, otherwise use the default filter
if (selectedRows.length > 0) {
filter = { [filterTargetKey]: ctx.blockModel.collection.getFilterByTK(selectedRows) };
} else {
filter = resource.getFilter(); // Default filter if no rows are selected
filter = resource.getFilter();
}
// If filterTargetKey indicates a complex filter (e.g., multiple keys), apply `$or`
if (Array.isArray(filterTargetKey) && filterTargetKey.length > 1) {
// Assuming getFilterByTK returns an array of conditions, apply $or
filter = { $or: ctx.blockModel.collection.getFilterByTK(selectedRows) };
}
exportSettings.forEach((es) => {
const { uiSchema, interface: fieldInterface } = fields.get(es.dataIndex[0]) ?? {};
// @ts-ignore
es.enum = uiSchema?.enum?.map((e) => ({ value: e.value, label: e.label }));
if (!es.enum && uiSchema?.type === 'boolean') {
es.enum = [
exportSettings.forEach((exportSetting) => {
const { uiSchema, interface: fieldInterface } = fields.get(exportSetting.dataIndex[0]) ?? {};
void fieldInterface;
exportSetting.enum = uiSchema?.enum?.map((item) => ({ value: item.value, label: item.label }));
if (!exportSetting.enum && uiSchema?.type === 'boolean') {
exportSetting.enum = [
{ value: true, label: ctx.t('Yes') },
{ value: false, label: ctx.t('No') },
];
}
es.defaultTitle = ctx.t(uiSchema?.title);
exportSetting.defaultTitle = ctx.t(uiSchema?.title);
});
const data = await resource.runAction('export', {
@@ -95,7 +95,7 @@ ExportActionModel.registerFlow({
title: ctx.t(title),
appends: resource.getAppends(),
sort: resource.getSort(),
filter: filter,
filter,
},
});
const blob = new Blob([data], { type: 'application/x-xls' });
@@ -151,7 +151,6 @@ ExportActionModel.registerFlow({
value: 'name',
children: 'children',
},
// labelInValue: true,
changeOnSelect: false,
options: data,
},
@@ -205,7 +204,7 @@ ExportActionModel.registerFlow({
const columns = exportSettings
?.filter((fieldItem) => fieldItem?.dataIndex?.length)
.map((item) => ({
dataIndex: item.dataIndex.map((di) => di.name ?? di),
dataIndex: item.dataIndex.map((dataIndex) => dataIndex.name ?? dataIndex),
title: item.title,
}));
ctx.model.setProps('exportSettings', columns);
@@ -0,0 +1,74 @@
/**
* 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.
*/
const TO_ONE_RELATION_TYPES = ['hasOne', 'belongsTo'];
const TO_MANY_RELATION_TYPES = ['hasMany', 'belongsToMany', 'belongsToArray'];
const RELATION_TYPES = [...TO_ONE_RELATION_TYPES, 'hasMany', 'belongsToMany', 'belongsToArray'];
const isRelationField = (field) => field?.target && RELATION_TYPES.includes(field.type);
const isToManyRelationField = (field) => field?.target && TO_MANY_RELATION_TYPES.includes(field.type);
const createOption = (field, getTitle, disabled = false) => ({
name: field.name,
title: getTitle(field),
schema: field?.uiSchema,
disabled,
});
const canNestRelationField = (parentField, childField, relationDepth) => {
if (relationDepth >= 2) {
return false;
}
if (isToManyRelationField(parentField) && isToManyRelationField(childField)) {
return false;
}
return true;
};
const buildFieldOption = (field, getTitle, getTargetFields, relationDepth = 0) => {
if (!field?.interface) {
return null;
}
if (!isRelationField(field)) {
return createOption(field, getTitle);
}
const targetFields = getTargetFields(field) || [];
const children = targetFields
.map((targetField) => {
if (!isRelationField(targetField)) {
return buildFieldOption(targetField, getTitle, getTargetFields);
}
if (!canNestRelationField(field, targetField, relationDepth)) {
return null;
}
return buildFieldOption(targetField, getTitle, getTargetFields, relationDepth + 1);
})
.filter(Boolean);
if (!children.length) {
return createOption(field, getTitle, true);
}
return {
...createOption(field, getTitle),
children,
};
};
export const buildExportFieldOptions = (fields, getTitle, getTargetFields) => {
return (fields || [])
.map((field) => buildFieldOption(field, getTitle, getTargetFields, isRelationField(field) ? 1 : 0))
.filter(Boolean);
};
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { buildExportFieldOptions } from '../buildExportFieldOptions';
import { buildExportFieldOptions } from './buildExportFieldOptions';
export const getOptionFields = (fields, t) => {
return buildExportFieldOptions(
@@ -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.
*/
import { Plugin } from '@nocobase/client-v2';
import { ExportActionModel } from './ExportActionModel';
export class PluginActionExportClient extends Plugin {
async load() {
this.app.flowEngine.registerModels({
ExportActionModel,
});
}
}
export { ExportActionModel } from './ExportActionModel';
export { buildExportFieldOptions } from './buildExportFieldOptions';
export default PluginActionExportClient;
@@ -7,4 +7,4 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
export * from './CustomRequestActionModel';
export const NAMESPACE = 'action-export';
@@ -7,69 +7,4 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
const TO_ONE_RELATION_TYPES = ['hasOne', 'belongsTo'];
const TO_MANY_RELATION_TYPES = ['hasMany', 'belongsToMany', 'belongsToArray'];
const RELATION_TYPES = [...TO_ONE_RELATION_TYPES, 'hasMany', 'belongsToMany', 'belongsToArray'];
const isRelationField = (field) => field?.target && RELATION_TYPES.includes(field.type);
const isToManyRelationField = (field) => field?.target && TO_MANY_RELATION_TYPES.includes(field.type);
const createOption = (field, getTitle, disabled = false) => ({
name: field.name,
title: getTitle(field),
schema: field?.uiSchema,
disabled,
});
const canNestRelationField = (parentField, childField, relationDepth) => {
if (relationDepth >= 2) {
return false;
}
if (isToManyRelationField(parentField) && isToManyRelationField(childField)) {
return false;
}
return true;
};
const buildFieldOption = (field, getTitle, getTargetFields, relationDepth = 0) => {
if (!field?.interface) {
return null;
}
if (!isRelationField(field)) {
return createOption(field, getTitle);
}
const targetFields = getTargetFields(field) || [];
const children = targetFields
.map((targetField) => {
if (!isRelationField(targetField)) {
return buildFieldOption(targetField, getTitle, getTargetFields);
}
if (!canNestRelationField(field, targetField, relationDepth)) {
// return createOption(targetField, getTitle, true);
return null;
}
return buildFieldOption(targetField, getTitle, getTargetFields, relationDepth + 1);
})
.filter(Boolean);
if (!children.length) {
return createOption(field, getTitle, true);
}
return {
...createOption(field, getTitle),
children,
};
};
export const buildExportFieldOptions = (fields, getTitle, getTargetFields) => {
return (fields || [])
.map((field) => buildFieldOption(field, getTitle, getTargetFields, isRelationField(field) ? 1 : 0))
.filter(Boolean);
};
export { buildExportFieldOptions } from '../client-v2/buildExportFieldOptions';
@@ -11,12 +11,12 @@ export * from './ExportActionInitializer';
export * from './ExportDesigner';
export * from './ExportPluginProvider';
export * from './useExportAction';
import { Plugin, useCollection, useActionAvailable } from '@nocobase/client';
import { Plugin, useActionAvailable } from '@nocobase/client';
import { ExportActionModel } from '../client-v2/ExportActionModel';
import { ExportPluginProvider } from './ExportPluginProvider';
import { exportActionSchemaSettings } from './schemaSettings';
import { ExportActionModel } from './models/ExportActionModel';
export { buildExportFieldOptions } from './buildExportFieldOptions';
export { buildExportFieldOptions } from '../client-v2/buildExportFieldOptions';
export class PluginActionExportClient extends Plugin {
async load() {
@@ -45,4 +45,5 @@ export class PluginActionExportClient extends Plugin {
}
}
export { ExportActionModel } from '../client-v2/ExportActionModel';
export default PluginActionExportClient;
@@ -8,8 +8,9 @@
*/
import { useTranslation } from 'react-i18next';
import { NAMESPACE } from '../../client-v2/locale';
export const NAMESPACE = 'action-export';
export { NAMESPACE };
export function useExportTranslation() {
return useTranslation([NAMESPACE, 'client'], { nsMode: 'fallback' });
@@ -0,0 +1,2 @@
export * from './dist/client-v2';
export { default } from './dist/client-v2';
@@ -0,0 +1 @@
module.exports = require('./dist/client-v2/index.js');
@@ -41,6 +41,7 @@
"peerDependencies": {
"@nocobase/actions": "2.x",
"@nocobase/client": "2.x",
"@nocobase/client-v2": "2.x",
"@nocobase/database": "2.x",
"@nocobase/server": "2.x",
"@nocobase/test": "2.x",
@@ -13,14 +13,14 @@ import { FormButtonGroup, FormItem, FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { createSchemaField, FormProvider } from '@formily/react';
import { observable } from '@formily/reactive';
import { Cascader, ActionModel, ActionSceneEnum } from '@nocobase/client';
import { ActionModel, ActionSceneEnum } from '@nocobase/client-v2';
import { escapeT, observer } from '@nocobase/flow-engine';
import { Button, Space, Spin, Upload } from 'antd';
import { Button, Cascader, Space, Spin, Upload } from 'antd';
import type { ButtonProps } from 'antd/es/button';
import { saveAs } from 'file-saver';
import React from 'react';
import { DownloadTips, ImportWarning } from '../ImportActionInitializer';
import { NAMESPACE } from '../constants';
import { DownloadTips, ImportWarning, initImportSettings } from './importSupport';
import { NAMESPACE } from './locale';
import { getOptionFields } from './getOptionFields';
const EXCLUDE_INTERFACES = ['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
@@ -30,13 +30,6 @@ const INCLUDE_FILE_TYPE = [
'application/wps-office.xlsx',
];
const initImportSettings = (fields) => {
const importColumns = fields
?.filter((f) => !f.isAssociationField() && !(EXCLUDE_INTERFACES.includes(f.interface) || !f.options.interface))
.map((f) => ({ dataIndex: [f.name] }));
return { importColumns, explain: '' };
};
const importFormSchema = {
type: 'object',
properties: {
@@ -73,7 +66,7 @@ const importFormSchema = {
'x-decorator': 'FormItem',
'x-acl-ignore': true,
'x-component': 'Upload.Dragger',
'x-validator': (value, rule) => {
'x-validator': (value) => {
const { fileList } = value;
if (fileList.length > 1) {
return {
@@ -95,9 +88,7 @@ const importFormSchema = {
height: '150px',
maxCount: 1,
children: escapeT('Upload placeholder', { ns: `${NAMESPACE}` }),
beforeUpload: () => {
return false;
},
beforeUpload: () => false,
},
},
},
@@ -121,6 +112,7 @@ export class ImportActionModel extends ActionModel {
type: 'default',
icon: 'uploadoutlined',
};
getAclActionName() {
return 'importXlsx';
}
@@ -129,21 +121,23 @@ export class ImportActionModel extends ActionModel {
ImportActionModel.define({
label: escapeT('Import'),
});
const toArr = (v: any) => {
if (!v || !Array.isArray(v)) {
const toArr = (value: any) => {
if (!value || !Array.isArray(value)) {
return [];
}
return v;
return value;
};
ImportActionModel.registerFlow({
key: 'importSettings',
title: escapeT('Import settings'),
on: 'click',
steps: {
import: {
handler: async (ctx, params) => {
handler: async (ctx) => {
const state = observable({
status: '', // '', 'importing', 'imported'
status: '',
result: null as any,
});
const form = createForm();
@@ -173,6 +167,7 @@ ImportActionModel.registerFlow({
return column;
})
.filter(Boolean);
const handelDownloadXlsxTemplateAction = async () => {
const data = await resource.runAction('downloadXlsxTemplate', {
data: {
@@ -191,7 +186,7 @@ ImportActionModel.registerFlow({
const { upload } = form.values;
const { fileList } = upload;
const formData = new FormData();
const uploadFiles = fileList.map((f) => f.originFileObj);
const uploadFiles = fileList.map((file) => file.originFileObj);
formData.append('file', uploadFiles[0]);
formData.append('columns', JSON.stringify(columns));
formData.append('explain', explain);
@@ -213,7 +208,7 @@ ImportActionModel.registerFlow({
await resource?.refresh?.();
form.reset();
} else {
popover.close(); // 后台任务直接关闭弹窗
popover.close();
form.reset();
}
} catch (error) {
@@ -226,7 +221,7 @@ ImportActionModel.registerFlow({
const { t } = ctx;
const renderResult = (result) => {
if (!result) return null;
const { successCount, meta } = result;
const { successCount } = result;
if (successCount) {
return t('{{successCount}} records have been successfully imported', { successCount, ns: NAMESPACE });
}
@@ -270,7 +265,6 @@ ImportActionModel.registerFlow({
);
}
// 初始导入表单
return (
<FormProvider form={form}>
<FormLayout layout="vertical">
@@ -295,9 +289,7 @@ ImportActionModel.registerFlow({
placement: 'center',
width: 800,
title: ctx.t('Import Data', { ns: `${NAMESPACE}` }),
content: (popover) => {
return <ImportDialogContent popover={popover} />;
},
content: (popover) => <ImportDialogContent popover={popover} />,
});
},
},
@@ -349,7 +341,6 @@ ImportActionModel.registerFlow({
'x-decorator': 'FormItem',
'x-component': Cascader,
required: true,
'x-component-props': {
fieldNames: {
label: 'title',
@@ -417,7 +408,7 @@ ImportActionModel.registerFlow({
const columns = importColumns
?.filter((fieldItem) => fieldItem?.dataIndex?.length)
.map((item) => ({
dataIndex: item.dataIndex.map((di) => di.name ?? di),
dataIndex: item.dataIndex.map((dataIndex) => dataIndex.name ?? dataIndex),
title: item.title,
description: item.description,
}));
@@ -0,0 +1,66 @@
/**
* 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.
*/
const EXCLUDE_INTERFACES = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
const RELATION_TYPES = ['hasOne', 'belongsTo', 'hasMany', 'belongsToMany', 'belongsToArray'];
const isRelationField = (field) => field?.target && RELATION_TYPES.includes(field.type);
const shouldExcludeField = (field) => !field?.interface || EXCLUDE_INTERFACES.includes(field.interface);
const createOption = (field, getTitle, disabled = false) => ({
name: field.name,
title: getTitle(field),
schema: field?.uiSchema,
disabled,
});
const buildFieldOption = (field, getTitle, getTargetFields, relationDepth = 0) => {
if (shouldExcludeField(field)) {
return null;
}
if (!isRelationField(field)) {
return createOption(field, getTitle);
}
const targetFields = getTargetFields(field) || [];
const children = targetFields
.map((targetField) => {
if (shouldExcludeField(targetField)) {
return null;
}
if (!isRelationField(targetField)) {
return buildFieldOption(targetField, getTitle, getTargetFields);
}
if (relationDepth >= 1) {
return createOption(targetField, getTitle, true);
}
return buildFieldOption(targetField, getTitle, getTargetFields, relationDepth + 1);
})
.filter(Boolean);
if (!children.length) {
return createOption(field, getTitle, true);
}
return {
...createOption(field, getTitle),
children,
};
};
export const buildImportFieldOptions = (fields, getTitle, getTargetFields) => {
return (fields || [])
.map((field) => buildFieldOption(field, getTitle, getTargetFields, isRelationField(field) ? 1 : 0))
.filter(Boolean);
};
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { buildImportFieldOptions } from '../buildImportFieldOptions';
import { buildImportFieldOptions } from './buildImportFieldOptions';
export const getOptionFields = (fields, t) => {
return buildImportFieldOptions(
@@ -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.
*/
import { Alert } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { NAMESPACE } from './locale';
export const initImportSettings = (fields) => {
const importColumns = fields
?.filter((field) => !field.children && !field.disabled)
.map((field) => ({ dataIndex: [field.name] }));
return { importColumns, explain: '' };
};
export const ImportWarning = () => {
const { t } = useTranslation([NAMESPACE, 'client'], { nsMode: 'fallback' });
return <Alert type="warning" style={{ marginBottom: '10px' }} message={t('Import warnings', { limit: 2000 })} />;
};
export const DownloadTips = () => {
const { t } = useTranslation([NAMESPACE, 'client'], { nsMode: 'fallback' });
return <Alert type="info" style={{ marginBottom: '10px', whiteSpace: 'pre-line' }} message={t('Download tips')} />;
};
@@ -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 { Plugin } from '@nocobase/client-v2';
import { ImportActionModel } from './ImportActionModel';
export class PluginActionImportClient extends Plugin {
async load() {
this.app.flowEngine.registerModels({ ImportActionModel });
}
}
export { buildImportFieldOptions } from './buildImportFieldOptions';
export { ImportActionModel } from './ImportActionModel';
export default PluginActionImportClient;
@@ -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 const NAMESPACE = 'action-import';
@@ -17,10 +17,9 @@ import {
useSchemaInitializerItem,
} from '@nocobase/client';
import React from 'react';
import { useImportTranslation } from './locale';
import { useFields } from './useFields';
import { Alert } from 'antd';
import { lodash } from '@nocobase/utils/client';
import { DownloadTips, ImportWarning, initImportSettings } from '../client-v2/importSupport';
const findSchema = (schema: Schema, key: string, action: string) => {
return schema.reduceProperties((buf, s) => {
@@ -35,11 +34,6 @@ const findSchema = (schema: Schema, key: string, action: string) => {
});
};
export const initImportSettings = (fields) => {
const importColumns = fields?.filter((f) => !f.children && !f.disabled).map((f) => ({ dataIndex: [f.name] }));
return { importColumns, explain: '' };
};
export const ImportActionInitializer = () => {
const itemConfig = useSchemaInitializerItem();
const { insert } = useSchemaInitializer();
@@ -67,13 +61,4 @@ export const ImportActionInitializer = () => {
/>
);
};
export const ImportWarning = () => {
const { t } = useImportTranslation();
return <Alert type="warning" style={{ marginBottom: '10px' }} message={t('Import warnings', { limit: 2000 })} />;
};
export const DownloadTips = () => {
const { t } = useImportTranslation();
return <Alert type="info" style={{ marginBottom: '10px', whiteSpace: 'pre-line' }} message={t('Download tips')} />;
};
export { DownloadTips, ImportWarning, initImportSettings };
@@ -7,60 +7,4 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
const EXCLUDE_INTERFACES = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
const RELATION_TYPES = ['hasOne', 'belongsTo', 'hasMany', 'belongsToMany', 'belongsToArray'];
const isRelationField = (field) => field?.target && RELATION_TYPES.includes(field.type);
const shouldExcludeField = (field) => !field?.interface || EXCLUDE_INTERFACES.includes(field.interface);
const createOption = (field, getTitle, disabled = false) => ({
name: field.name,
title: getTitle(field),
schema: field?.uiSchema,
disabled,
});
const buildFieldOption = (field, getTitle, getTargetFields, relationDepth = 0) => {
if (shouldExcludeField(field)) {
return null;
}
if (!isRelationField(field)) {
return createOption(field, getTitle);
}
const targetFields = getTargetFields(field) || [];
const children = targetFields
.map((targetField) => {
if (shouldExcludeField(targetField)) {
return null;
}
if (!isRelationField(targetField)) {
return buildFieldOption(targetField, getTitle, getTargetFields);
}
if (relationDepth >= 1) {
return createOption(targetField, getTitle, true);
}
return buildFieldOption(targetField, getTitle, getTargetFields, relationDepth + 1);
})
.filter(Boolean);
if (!children.length) {
return createOption(field, getTitle, true);
}
return {
...createOption(field, getTitle),
children,
};
};
export const buildImportFieldOptions = (fields, getTitle, getTargetFields) => {
return (fields || [])
.map((field) => buildFieldOption(field, getTitle, getTargetFields, isRelationField(field) ? 1 : 0))
.filter(Boolean);
};
export { buildImportFieldOptions } from '../client-v2/buildImportFieldOptions';
@@ -7,4 +7,4 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
export const NAMESPACE = 'action-import';
export { NAMESPACE } from '../client-v2/locale';
@@ -16,11 +16,11 @@ export * from './ImportPluginProvider';
export * from './useImportAction';
import { Plugin, useActionAvailable } from '@nocobase/client';
import { ImportActionModel } from '../client-v2/ImportActionModel';
import { ImportPluginProvider } from './ImportPluginProvider';
import { importActionSchemaSettings } from './schemaSettings';
import { ImportActionModel } from './models/ImportActionModel';
export { buildImportFieldOptions } from './buildImportFieldOptions';
export { buildImportFieldOptions } from '../client-v2/buildImportFieldOptions';
export class PluginActionImportClient extends Plugin {
async load() {
@@ -49,4 +49,5 @@ export class PluginActionImportClient extends Plugin {
}
}
export { ImportActionModel } from '../client-v2/ImportActionModel';
export default PluginActionImportClient;
@@ -8,8 +8,9 @@
*/
import { useTranslation } from 'react-i18next';
import { NAMESPACE } from '../../client-v2/locale';
export const NAMESPACE = 'action-import';
export { NAMESPACE };
export function useImportTranslation() {
return useTranslation([NAMESPACE, 'client'], { nsMode: 'fallback' });
@@ -26,9 +26,6 @@ import { NAMESPACE } from './constants';
import { useImportContext } from './context';
import { ImportStatus } from './ImportModal';
import { useEffect } from 'react';
import { useFields } from './useFields';
import { initImportSettings } from './ImportActionInitializer';
import { useImportActionContext } from './ImportActionContext';
const useImportSchema = () => {
const { fieldSchema: actionSchema } = useActionContext();
@@ -0,0 +1,2 @@
export * from './dist/client-v2';
export { default } from './dist/client-v2';
@@ -0,0 +1 @@
module.exports = require('./dist/client-v2/index.js');
@@ -10,6 +10,7 @@
"description.zh-CN": "支持 URL 格式的附件。",
"peerDependencies": {
"@nocobase/client": "2.x",
"@nocobase/client-v2": "2.x",
"@nocobase/plugin-file-manager": "2.x",
"@nocobase/server": "2.x",
"@nocobase/test": "2.x"
@@ -7,28 +7,29 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { UploadFieldModel } from '@nocobase/plugin-file-manager/client';
import { Upload } from '@formily/antd-v5';
import { css } from '@emotion/css';
import { UploadOutlined } from '@ant-design/icons';
import { DisplayItemModel, EditableItemModel, tExpr } from '@nocobase/flow-engine';
import React, { useEffect, useState } from 'react';
import { FieldContext } from '@formily/react';
import { css } from '@emotion/css';
import { Upload } from '@formily/antd-v5';
import { FieldContext, useField } from '@formily/react';
import { DisplayItemModel, EditableItemModel } from '@nocobase/flow-engine';
import { UploadFieldModel } from '@nocobase/plugin-file-manager/client-v2';
import { castArray } from 'lodash';
import { useField } from '@formily/react';
import React, { useEffect, useState } from 'react';
import { tExpr } from './locale';
const CardUpload = (props) => {
const { showFileName, value, onChange } = props;
const outerField: any = useField();
const [fileList, setFileList] = useState(
castArray(value || []).map((v) => {
return { url: v };
castArray(value || []).map((item) => {
return { url: item };
}),
);
useEffect(() => {
setFileList(
castArray(value || []).map((v) => {
return { url: v };
castArray(value || []).map((item) => {
return { url: item };
}),
);
}, [value]);
@@ -47,6 +48,7 @@ const CardUpload = (props) => {
onChange?.(undefined);
}
};
return (
<FieldContext.Provider
value={{
@@ -101,13 +103,16 @@ const CardUpload = (props) => {
</FieldContext.Provider>
);
};
export class AttachmentURLFieldModel extends UploadFieldModel {
declare props: Record<string, any>;
render() {
return <CardUpload {...this.props} />;
}
}
AttachmentURLFieldModel.define({
(AttachmentURLFieldModel as any).define({
label: tExpr('AttachmentURL'),
});
@@ -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.
*/
import { Application, Plugin } from '@nocobase/client-v2';
import { AttachmentURLFieldInterface } from './interfaces/attachment-url';
export class PluginFieldAttachmentUrlClient extends Plugin<any, Application> {
async load() {
this.app.addFieldInterfaces([AttachmentURLFieldInterface]);
this.flowEngine.registerModelLoaders({
AttachmentURLFieldModel: {
loader: () => import('./AttachmentURLFieldModel'),
},
});
}
}
export { AttachmentURLFieldInterface } from './interfaces/attachment-url';
export { AttachmentURLFieldModel } from './AttachmentURLFieldModel';
export default PluginFieldAttachmentUrlClient;
@@ -0,0 +1,141 @@
/**
* 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 type { ISchema } from '@formily/react';
import { useField } from '@formily/react';
import { CollectionFieldInterface } from '@nocobase/client-v2';
const attachmentUrlOperators = [
{
label: '{{t("contains")}}',
value: '$includes',
selected: true,
schema: {
type: 'string',
'x-component': 'Input',
},
},
{
label: '{{t("does not contain")}}',
value: '$notIncludes',
schema: {
type: 'string',
'x-component': 'Input',
},
},
{
label: '{{t("is")}}',
value: '$eq',
schema: {
type: 'string',
'x-component': 'Input',
},
},
{
label: '{{t("is not")}}',
value: '$ne',
schema: {
type: 'string',
'x-component': 'Input',
},
},
{
label: '{{t("is empty")}}',
value: '$empty',
noValue: true,
schema: {
type: 'string',
'x-component': 'Input',
},
},
{
label: '{{t("is not empty")}}',
value: '$notEmpty',
noValue: true,
schema: {
type: 'string',
'x-component': 'Input',
},
},
];
const useAttachmentTargetProps = () => {
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;
},
};
};
export class AttachmentURLFieldInterface extends CollectionFieldInterface {
name = 'attachmentURL';
type = 'object';
group = 'media';
title = '{{t("Attachment (URL)")}}';
default = {
interface: 'attachmentURL',
type: 'string',
uiSchema: {
type: 'string',
'x-component': 'Input',
},
};
availableTypes = ['string', 'text'];
validationType = 'string';
availableValidationOptions = ['min', 'max', 'length', 'pattern'];
properties = {
target: {
required: true,
type: 'string',
title: '{{t("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((option) => option?.name === 'attachments');
if (hasAttachments && !field.initialValue) {
field.setInitialValue('attachments');
}
},
},
targetKey: {
type: 'string',
default: 'id',
'x-hidden': true,
},
};
filterable = {
nested: true,
operators: attachmentUrlOperators,
};
titleUsable = true;
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';
}
}
}
@@ -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 { tExpr as _tExpr, useFlowEngine } from '@nocobase/flow-engine';
// @ts-ignore
import pkg from '../../package.json';
export function useT() {
const engine = useFlowEngine();
return (str: string) => engine.context.t(str, { ns: [pkg.name, 'client'] });
}
export function tExpr(key: string) {
return _tExpr(key, { ns: [pkg.name, 'client'] });
}
@@ -10,7 +10,7 @@
import { Plugin, lazy } from '@nocobase/client';
import { AttachmentURLFieldInterface } from './interfaces/attachment-url';
import { useAttachmentUrlFieldProps } from './hook';
import { AttachmentURLFieldModel } from './fieldModel/AttachmentURLFieldModel';
import { AttachmentURLFieldModel } from '../client-v2/AttachmentURLFieldModel';
const { AttachmentUrl } = lazy(() => import('./component/AttachmentUrl'), 'AttachmentUrl');
import { attachmentUrlComponentFieldSettings } from './settings';
@@ -27,9 +27,10 @@ export class PluginFieldAttachmentUrlClient extends Plugin {
this.app.addComponents({ AttachmentUrl });
this.app.schemaSettingsManager.add(attachmentUrlComponentFieldSettings);
this.flowEngine.registerModels({
AttachmentURLFieldModel,
AttachmentURLFieldModel: AttachmentURLFieldModel as any,
});
}
}
export { AttachmentURLFieldModel } from '../client-v2/AttachmentURLFieldModel';
export default PluginFieldAttachmentUrlClient;
@@ -0,0 +1,2 @@
export * from './dist/client-v2';
export { default } from './dist/client-v2';

Some files were not shown because too many files have changed in this diff Show More