Merge branch 'main' into next

This commit is contained in:
nocobase[bot] 2025-03-25 01:39:52 +00:00
commit e97197746b
12 changed files with 1144 additions and 1189 deletions

File diff suppressed because it is too large Load Diff

View File

@ -126,6 +126,10 @@ export const getAllowMultiple = (params?: { title: string }) => {
return {
name: 'allowMultiple',
type: 'switch',
useVisible() {
const isAssociationField = useIsAssociationField();
return isAssociationField;
},
useComponentProps() {
const { t } = useTranslation();
const field = useField<Field>();

View File

@ -14,6 +14,14 @@ import React, { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Router } from 'react-router-dom';
import { SchemaInitializerItem } from '../../application';
import {
CollectionManagerProvider,
useCollectionManager,
} from '../../data-source/collection/CollectionManagerProvider';
import {
DataSourceManagerProvider,
useDataSourceManager,
} from '../../data-source/data-source/DataSourceManagerProvider';
import { useGlobalTheme } from '../../global-theme';
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
import {
@ -34,6 +42,8 @@ export const LinkMenuItem = () => {
const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
const parentRoute = useParentRoute();
const { createRoute } = useNocoBaseRoutes();
const dm = useDataSourceManager();
const cm = useCollectionManager();
const handleClick = useCallback(async () => {
const values = await FormDialog(
@ -41,31 +51,35 @@ export const LinkMenuItem = () => {
() => {
const history = createMemoryHistory();
return (
<Router location={history.location} navigator={history}>
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
<FormLayout layout={'vertical'}>
<SchemaComponent
schema={{
properties: {
title: {
title: t('Menu item title'),
required: true,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
icon: {
title: t('Icon'),
'x-component': 'IconPicker',
'x-decorator': 'FormItem',
},
href: urlSchema,
params: paramsSchema,
},
}}
/>
</FormLayout>
</SchemaComponentOptions>
</Router>
<DataSourceManagerProvider dataSourceManager={dm}>
<CollectionManagerProvider instance={cm} dataSource={cm?.dataSource?.key}>
<Router location={history.location} navigator={history}>
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
<FormLayout layout={'vertical'}>
<SchemaComponent
schema={{
properties: {
title: {
title: t('Menu item title'),
required: true,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
icon: {
title: t('Icon'),
'x-component': 'IconPicker',
'x-decorator': 'FormItem',
},
href: urlSchema,
params: paramsSchema,
},
}}
/>
</FormLayout>
</SchemaComponentOptions>
</Router>
</CollectionManagerProvider>
</DataSourceManagerProvider>
);
},
theme,

View File

@ -14,22 +14,22 @@ import { uid } from '@formily/shared';
import { Space, message } from 'antd';
import { isEqual } from 'lodash';
import { isFunction } from 'mathjs';
import React, { useEffect, useState, useContext } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ClearCollectionFieldContext,
NocoBaseRecursionField,
RecordProvider,
SchemaComponentContext,
useAPIClient,
useCollectionRecordData,
SchemaComponentContext,
} from '../../../';
import { Action } from '../action';
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
import { isVariable } from '../../../variables/utils/isVariable';
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { Action } from '../action';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions, { useAssociationFieldContext } from './hooks';
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
export const AssociationFieldAddNewer = (props) => {
const schemaComponentCtxValue = useContext(SchemaComponentContext);

View File

@ -53,6 +53,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
};
}
// 用于获取关系字段请求数据时所需的一些参数
export default function useServiceOptions(props) {
const { action = 'list', service, useOriginalFilter } = props;
const fieldSchema = useFieldSchema();

View File

@ -92,7 +92,7 @@ export const useGetFilterFieldOptions = () => {
const getOptions = (fields, depth, usedInVariable?: boolean) => {
const options = [];
fields.forEach((field) => {
fields?.forEach((field) => {
const option = field2option(field, depth, usedInVariable);
if (option) {
options.push(option);

View File

@ -1,5 +0,0 @@
# Page
Can be used in conjunction with DocumentTitleProvider to display the page title on document.title.
<code src="./demos/demo1.tsx"></code>

View File

@ -1,5 +0,0 @@
# Page
可以与 DocumentTitleProvider 搭配使用,将 page title 显示在 document.title 上。
<code src="./demos/demo1.tsx"></code>

View File

@ -18,7 +18,6 @@ import { getDataSourceHeaders } from '../data-source/utils';
import { useCompile } from '../schema-component';
import useBuiltInVariables from './hooks/useBuiltinVariables';
import { VariableOption, VariablesContextType } from './types';
import { cacheLazyLoadedValues, getCachedLazyLoadedValues } from './utils/cacheLazyLoadedValues';
import { filterEmptyValues } from './utils/filterEmptyValues';
import { getAction } from './utils/getAction';
import { getPath } from './utils/getPath';
@ -144,14 +143,13 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
.then((data) => {
clearRequested(url);
const value = data.data.data;
cacheLazyLoadedValues(item, currentVariablePath, value);
return value;
});
stashRequested(url, result);
return result;
}
}
return getCachedLazyLoadedValues(item, currentVariablePath) || item?.[key];
return item?.[key];
});
current = removeThroughCollectionFields(_.flatten(await Promise.all(result)), associationField);
} else if (
@ -180,17 +178,9 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
}
const value = data.data.data;
if (!getCachedLazyLoadedValues(current, currentVariablePath)) {
// Cache the API response data to avoid repeated requests
cacheLazyLoadedValues(current, currentVariablePath, value);
}
current = removeThroughCollectionFields(value, associationField);
} else {
current = removeThroughCollectionFields(
getCachedLazyLoadedValues(current, currentVariablePath) || getValuesByPath(current, key),
associationField,
);
current = removeThroughCollectionFields(getValuesByPath(current, key), associationField);
}
if (associationField?.target) {
@ -359,13 +349,8 @@ export default VariablesProvider;
function shouldToRequest(value, variableCtx: Record<string, any>, variablePath: string) {
let result = false;
if (getCachedLazyLoadedValues(variableCtx, variablePath)) {
return false;
}
// value may be a reactive object, using untracked to avoid unexpected autorun
untracked(() => {
// fix https://nocobase.height.app/T-2502
// Compatible with `xxx to many` and `xxx to one` subform fields and subtable fields
if (JSON.stringify(value) === '[{}]' || JSON.stringify(value) === '{}') {
result = true;

View File

@ -1,25 +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.
*/
const cache = new Map<Record<string, any>, any>();
export const cacheLazyLoadedValues = (variableCtx: Record<string, any>, variablePath: string, value: any) => {
const cachedValue = cache.get(variableCtx);
if (cachedValue) {
cachedValue[variablePath] = value;
} else {
cache.set(variableCtx, { [variablePath]: value });
}
};
export const getCachedLazyLoadedValues = (variableCtx: Record<string, any>, variablePath: string) => {
const cachedValue = cache.get(variableCtx);
return cachedValue?.[variablePath];
};

View File

@ -67,10 +67,13 @@ export const useCustomizeRequestActionProps = () => {
responseType: fieldSchema['x-response-type'] === 'stream' ? 'blob' : 'json',
});
if (res.headers['content-disposition']) {
const regex = /attachment;\s*filename="([^"]+)"/;
const match = res.headers['content-disposition'].match(regex);
if (match?.[1]) {
saveAs(res.data, match[1]);
const contentDisposition = res.headers['content-disposition'];
const utf8Match = contentDisposition.match(/filename\*=utf-8''([^;]+)/i);
const asciiMatch = contentDisposition.match(/filename="([^"]+)"/i);
if (utf8Match) {
saveAs(res.data, decodeURIComponent(utf8Match[1]));
} else if (asciiMatch) {
saveAs(res.data, asciiMatch[1]);
}
}
actionField.data.loading = false;

View File

@ -31,6 +31,8 @@ export default class extends Migration {
await this.db.sequelize.transaction(async (transaction) => {
const treeCollections = await this.getTreeCollections({ transaction });
for (const treeCollection of treeCollections) {
await treeCollection.load({ transaction });
const name = `main_${treeCollection.name}_path`;
const collectionOptions = {
name,
@ -47,35 +49,16 @@ export default class extends Migration {
},
],
};
const collectionInstance = this.db.getCollection(treeCollection.name);
const treeCollectionSchema = collectionInstance.collectionSchema();
if (this.app.db.inDialect('postgres') && treeCollectionSchema != this.app.db.options.schema) {
collectionOptions['schema'] = treeCollectionSchema;
}
this.app.db.collection(collectionOptions);
const treeExistsInDb = await this.app.db.getCollection(name).existsInDb({ transaction });
if (!treeExistsInDb) {
await this.app.db.getCollection(name).sync({ transaction } as SyncOptions);
const opts = {
name: treeCollection.name,
autoGenId: false,
timestamps: false,
fields: [
{ type: 'integer', name: 'id' },
{ type: 'integer', name: 'parentId' },
],
};
if (treeCollectionSchema != this.app.db.options.schema) {
opts['schema'] = treeCollectionSchema;
}
this.app.db.collection(opts);
const chunkSize = 1000;
await this.app.db.getRepository(treeCollection.name).chunk({
chunkSize: chunkSize,