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) && (