diff --git a/packages/core/client/src/application/components/defaultComponents.tsx b/packages/core/client/src/application/components/defaultComponents.tsx
index aeb2c475b9..31801e97ed 100644
--- a/packages/core/client/src/application/components/defaultComponents.tsx
+++ b/packages/core/client/src/application/components/defaultComponents.tsx
@@ -11,10 +11,11 @@ import React, { FC } from 'react';
import { MainComponent } from './MainComponent';
const Loading: FC = () =>
Loading...
;
-const AppError: FC<{ error: Error }> = ({ error }) => {
+const AppError: FC<{ error: Error & { title?: string } }> = ({ error }) => {
+ const title = error?.title || 'App Error';
return (
-
App Error
+
{title}
{error?.message}
{process.env.__TEST__ && error?.stack}
diff --git a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts
index ce0b6d411f..80b4e31527 100644
--- a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts
+++ b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts
@@ -129,12 +129,12 @@ export const enumType = [
label: '{{t("is")}}',
value: '$eq',
selected: true,
- schema: { 'x-component': 'Select' },
+ schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
},
{
label: '{{t("is not")}}',
value: '$ne',
- schema: { 'x-component': 'Select' },
+ schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
},
{
label: '{{t("is any of")}}',
diff --git a/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx b/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx
index 2f075ecaf3..a3e45281d1 100644
--- a/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx
+++ b/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx
@@ -96,7 +96,7 @@ export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
const originalProps =
compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {};
- field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
+ field.componentProps = merge(field.componentProps || {}, originalProps, dynamicProps || {});
}, [uiSchemaOrigin]);
if (!uiSchemaOrigin) return null;
diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx
index cc706c760a..c53173b30a 100644
--- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx
+++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx
@@ -74,7 +74,7 @@ const useErrorProps = (app: Application, error: any) => {
}
};
-const AppError: FC<{ error: Error; app: Application }> = observer(
+const AppError: FC<{ error: Error & { title?: string }; app: Application }> = observer(
({ app, error }) => {
const props = getProps(app);
return (
@@ -87,7 +87,7 @@ const AppError: FC<{ error: Error; app: Application }> = observer(
transform: translate(0, -50%);
`}
status="error"
- title={app.i18n.t('App error')}
+ title={error?.title || app.i18n.t('App error', { ns: 'client' })}
subTitle={app.i18n.t(error?.message)}
{...props}
extra={[
diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx
index 190a5e6f23..740d66fd5f 100644
--- a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx
+++ b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx
@@ -14,6 +14,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useAPIClient, useRequest } from '../../../api-client';
import { useCollectionManager } from '../../../data-source/collection';
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
+import { getDataSourceHeaders } from '../../../data-source/utils';
import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
import { useSchemaComponentContext } from '../../hooks';
import { AssociationFieldContext } from './context';
@@ -67,9 +68,11 @@ export const AssociationFieldProvider = observer(
if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) {
return Promise.reject(null);
}
+
return api.request({
resource: collectionField.target,
action: Array.isArray(ids) ? 'list' : 'get',
+ headers: getDataSourceHeaders(cm?.dataSource?.key),
params: {
filter: {
[targetKey]: ids,
diff --git a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx
index 79025dabd2..2c5062344e 100644
--- a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx
+++ b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx
@@ -65,7 +65,6 @@ export const DynamicComponent = (props: Props) => {
...props.style,
},
utc: false,
- underFilter: true,
}),
name: 'value',
'x-read-pretty': false,
diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx
index d55ec37a5a..a7a2ef2af3 100644
--- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx
+++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx
@@ -92,7 +92,6 @@ export const FormItem: any = withDynamicSchemaProps(
[formItemLabelCss]: showTitle === false,
});
}, [showTitle]);
- console.log(className);
// 联动规则中的“隐藏保留值”的效果
if (field.data?.hidden) {
return null;
diff --git a/packages/core/client/src/schema-component/antd/input/Input.tsx b/packages/core/client/src/schema-component/antd/input/Input.tsx
index 1a7a072ca2..2870f7abf9 100644
--- a/packages/core/client/src/schema-component/antd/input/Input.tsx
+++ b/packages/core/client/src/schema-component/antd/input/Input.tsx
@@ -11,22 +11,40 @@ import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Input as AntdInput } from 'antd';
import { InputProps, TextAreaProps } from 'antd/es/input';
-import React from 'react';
+import React, { useCallback } from 'react';
import { JSONTextAreaProps, Json } from './Json';
import { InputReadPrettyComposed, ReadPretty } from './ReadPretty';
export { ReadPretty as InputReadPretty } from './ReadPretty';
-type ComposedInput = React.FC & {
+type ComposedInput = React.FC & {
ReadPretty: InputReadPrettyComposed['Input'];
TextArea: React.FC & { ReadPretty: InputReadPrettyComposed['TextArea'] };
URL: React.FC & { ReadPretty: InputReadPrettyComposed['URL'] };
JSON: React.FC & { ReadPretty: InputReadPrettyComposed['JSON'] };
};
+export type NocoBaseInputProps = InputProps & {
+ trim?: boolean;
+};
+
+function InputInner(props: NocoBaseInputProps) {
+ const { onChange, trim, ...others } = props;
+ const handleChange = useCallback(
+ (ev: React.ChangeEvent) => {
+ if (trim) {
+ ev.target.value = ev.target.value.trim();
+ }
+ onChange?.(ev);
+ },
+ [onChange, trim],
+ );
+ return ;
+}
+
export const Input: ComposedInput = Object.assign(
connect(
- AntdInput,
+ InputInner,
mapProps((props, field) => {
return {
...props,
diff --git a/packages/core/client/src/schema-component/antd/input/demos/new-demos/input.tsx b/packages/core/client/src/schema-component/antd/input/demos/new-demos/input.tsx
index 053a736c4c..5fba3f3699 100644
--- a/packages/core/client/src/schema-component/antd/input/demos/new-demos/input.tsx
+++ b/packages/core/client/src/schema-component/antd/input/demos/new-demos/input.tsx
@@ -1,4 +1,3 @@
-
import React from 'react';
import { mockApp } from '@nocobase/client/demo-utils';
import { SchemaComponent, Plugin, ISchema } from '@nocobase/client';
@@ -15,15 +14,25 @@ const schema: ISchema = {
'x-decorator': 'FormItem',
'x-component': 'Input',
},
+ trim: {
+ type: 'string',
+ title: `Trim heading and tailing spaces`,
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input',
+ 'x-component-props': {
+ trim: true,
+ },
+ },
},
-}
+};
+
const Demo = () => {
return ;
};
class DemoPlugin extends Plugin {
async load() {
- this.app.router.add('root', { path: '/', Component: Demo })
+ this.app.router.add('root', { path: '/', Component: Demo });
}
}
diff --git a/packages/core/client/src/schema-component/antd/variable/TextArea.tsx b/packages/core/client/src/schema-component/antd/variable/TextArea.tsx
index bccaa60c39..1a43e849b4 100644
--- a/packages/core/client/src/schema-component/antd/variable/TextArea.tsx
+++ b/packages/core/client/src/schema-component/antd/variable/TextArea.tsx
@@ -10,6 +10,7 @@
import { css, cx } from '@emotion/css';
import { useForm } from '@formily/react';
import { Space, theme } from 'antd';
+import type { CascaderProps, DefaultOptionType } from 'antd/lib/cascader';
import useInputStyle from 'antd/es/input/style';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { renderToString } from 'react-dom/server';
@@ -110,7 +111,7 @@ function renderHTML(exp: string, keyLabelMap, delimiters: [string, string] = ['{
});
}
-function createOptionsValueLabelMap(options: any[], fieldNames = { value: 'value', label: 'label' }) {
+function createOptionsValueLabelMap(options: any[], fieldNames: CascaderProps['fieldNames'] = defaultFieldNames) {
const map = new Map();
for (const option of options) {
map.set(option[fieldNames.value], [option[fieldNames.label]]);
@@ -220,10 +221,24 @@ function useVariablesFromValue(value: string, delimiters: [string, string] = ['{
}, [value, delimitersString]);
}
-export function TextArea(props) {
+export type TextAreaProps = {
+ value?: string;
+ scope?: Partial[] | (() => Partial[]);
+ onChange?(value: string): void;
+ disabled?: boolean;
+ changeOnSelect?: CascaderProps['changeOnSelect'];
+ style?: React.CSSProperties;
+ fieldNames?: CascaderProps['fieldNames'];
+ trim?: boolean;
+ delimiters?: [string, string];
+ addonBefore?: React.ReactNode;
+};
+
+export function TextArea(props: TextAreaProps) {
const { wrapSSR, hashId, componentCls } = useStyles();
- const { scope, onChange, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'], addonBefore } = props;
- const value = typeof props.value === 'string' ? props.value : props.value == null ? '' : props.value.toString();
+ const { scope, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'], addonBefore, trim = true } = props;
+ const value =
+ typeof props.value === 'string' ? props.value : props.value == null ? '' : (props.value as any).toString();
const variables = useVariablesFromValue(value, delimiters);
const inputRef = useRef(null);
const [options, setOptions] = useState([]);
@@ -241,6 +256,14 @@ export function TextArea(props) {
const { token } = theme.useToken();
const delimitersString = delimiters.join(' ');
+ const onChange = useCallback(
+ (target: HTMLDivElement) => {
+ const v = getValue(target, delimiters);
+ props.onChange?.(trim ? v.trim() : v);
+ },
+ [delimitersString, props.onChange, trim],
+ );
+
useEffect(() => {
preloadOptions(scope, variables)
.then((preloaded) => {
@@ -324,9 +347,9 @@ export function TextArea(props) {
setChanged(true);
setRange(getCurrentRange(current));
- onChange(getValue(current, delimiters));
+ onChange(current);
},
- [keyLabelMap, onChange, range, delimitersString],
+ [keyLabelMap, onChange, range],
);
const onInput = useCallback(
@@ -336,9 +359,9 @@ export function TextArea(props) {
}
setChanged(true);
setRange(getCurrentRange(currentTarget));
- onChange(getValue(currentTarget, delimiters));
+ onChange(currentTarget);
},
- [ime, onChange, delimitersString],
+ [ime, onChange],
);
const onBlur = useCallback(function ({ currentTarget }) {
@@ -360,9 +383,9 @@ export function TextArea(props) {
setIME(false);
setChanged(true);
setRange(getCurrentRange(currentTarget));
- onChange(getValue(currentTarget, delimiters));
+ onChange(currentTarget);
},
- [onChange, delimitersString],
+ [onChange],
);
const onPaste = useCallback(
@@ -393,9 +416,9 @@ export function TextArea(props) {
setChanged(true);
pasteHTML(ev.currentTarget, sanitizedHTML);
setRange(getCurrentRange(ev.currentTarget));
- onChange(getValue(ev.currentTarget, delimiters));
+ onChange(ev.currentTarget);
},
- [onChange, delimitersString],
+ [onChange],
);
const disabled = props.disabled || form.disabled;
return wrapSSR(
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..bae058d2a0 100644
--- a/packages/core/client/src/schema-component/common/utils/uitls.tsx
+++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx
@@ -96,7 +96,6 @@ export const conditionAnalyses = async (
) => {
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);
@@ -147,7 +146,10 @@ export const conditionAnalyses = async (
if (type === '$and') {
return every(results, (v) => v);
} else {
- return some(results, (v) => v);
+ if (results.length) {
+ return some(results, (v) => v);
+ }
+ return true;
}
};
diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts
index ac86cd5a75..66520a4110 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 {
diff --git a/packages/core/client/src/variables/utils/isVariable.tsx b/packages/core/client/src/variables/utils/isVariable.tsx
index 7e97d9e077..57a125c71f 100644
--- a/packages/core/client/src/variables/utils/isVariable.tsx
+++ b/packages/core/client/src/variables/utils/isVariable.tsx
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-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/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/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-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-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-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) && (