feat: amis 添加更多扩展功能 (#10924)

This commit is contained in:
liaoxuezhi 2024-09-18 19:03:49 +08:00 committed by GitHub
parent e0460bf080
commit cb5e5dca5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1276 additions and 161 deletions

View File

@ -14,6 +14,14 @@ import {render as renderAmis, makeTranslator} from 'amis-core';
import 'amis/lib/minimal';
import 'amis-ui/lib/locale/en-US';
import 'amis-ui/lib/locale/zh-CN';
import 'amis-ui/lib/locale/en-US';
import 'amis-ui/lib/locale/de-DE';
import 'amis-ui/lib/themes/cxd';
import 'amis-ui/lib/themes/ang';
import 'amis-ui/lib/themes/antd';
import 'amis-ui/lib/themes/dark';
import 'history';
import {attachmentAdpator} from 'amis-core';
import {pdfUrlLoad} from './loadPdfjsWorker';

View File

@ -55,3 +55,10 @@ global.afterAll(() => {
console.error = originalError;
cleanup();
});
// expect.addSnapshotSerializer({
// test: val => typeof val === 'string' && /^[a-z0-9]{12}$/.test(val),
// print: val => {
// return JSON.stringify('__guid_dynamic_value__');
// }
// });

View File

@ -7,9 +7,8 @@
import React from 'react';
import {findDOMNode} from 'react-dom';
import {ClassNamesFn, themeable} from '../theme';
import {autobind, camel, preventDefault} from '../utils';
import {autobind, camel, preventDefault, TestIdBuilder} from '../utils';
import {SubPopoverDisplayedID} from './Overlay';
import type {TestIdBuilder} from 'amis-core';
export interface Offset {
x: number;

View File

@ -195,7 +195,8 @@ function rendererToComponent(
export function registerRenderer(config: RendererConfig): RendererConfig {
if (!config.test && !config.type) {
throw new TypeError('please set config.type or config.test');
} else if (!config.type) {
} else if (!config.type && config.name !== 'static') {
// todo static 目前还没办法不用 test 来实现
console.warn(
`config.type is recommended for register renderer(${config.test})`
);

View File

@ -39,7 +39,7 @@ export * from './store';
import * as utils from './utils/helper';
import {getEnv} from 'mobx-state-tree';
import {RegisterStore, RendererStore} from './store';
import {RegisterStore, registerStore, RendererStore} from './store';
import type {IColumn, IColumn2, IRow, IRow2} from './store';
import {
setDefaultLocale,
@ -133,6 +133,7 @@ export {
RendererEnv,
EnvContext,
RegisterStore,
registerStore,
FormItem,
FormItemWrap,
FormItemProps,

View File

@ -114,6 +114,7 @@ export {iRendererStore, IIRendererStore};
export const RegisterStore = function (store: any) {
allowedStoreList.push(store as any);
};
export const registerStore = RegisterStore;
export {
ServiceStore,

View File

@ -216,6 +216,18 @@ export type ClassName =
[propName: string]: boolean | undefined | null | string;
};
export type RequestAdaptor = (
api: ApiObject,
context: any
) => ApiObject | Promise<ApiObject>;
export type ResponseAdaptor = (
payload: object,
response: fetcherResult,
api: ApiObject,
context: any
) => any;
export interface ApiObject extends BaseApiObject {
config?: {
withCredentials?: boolean;
@ -228,16 +240,8 @@ export interface ApiObject extends BaseApiObject {
body?: PlainObject;
query?: PlainObject;
mockResponse?: PlainObject;
adaptor?: (
payload: object,
response: fetcherResult,
api: ApiObject,
context: any
) => any;
requestAdaptor?: (
api: ApiObject,
context: any
) => ApiObject | Promise<ApiObject>;
adaptor?: ResponseAdaptor;
requestAdaptor?: RequestAdaptor;
/**
* api api adaptor
* @readonly

View File

@ -1,5 +1,13 @@
import omit from 'lodash/omit';
import {Api, ApiObject, EventTrack, fetcherResult, Payload} from '../types';
import {
Api,
ApiObject,
EventTrack,
fetcherResult,
Payload,
RequestAdaptor,
ResponseAdaptor
} from '../types';
import {FetcherConfig} from '../factory';
import {tokenize, dataMapping, escapeHtml} from './tpl-builtin';
import {evalExpression} from './tpl';
@ -33,6 +41,44 @@ interface ApiCacheConfig extends ApiObject {
}
const apiCaches: Array<ApiCacheConfig> = [];
const requestAdaptors: Array<RequestAdaptor> = [];
const responseAdaptors: Array<ResponseAdaptor> = [];
/**
*
* @param adaptor
*/
export function addApiRequestAdaptor(adaptor: RequestAdaptor) {
requestAdaptors.push(adaptor);
return () => removeApiRequestAdaptor(adaptor);
}
/**
*
* @param adaptor
*/
export function removeApiRequestAdaptor(adaptor: RequestAdaptor) {
const idx = requestAdaptors.findIndex(i => i === adaptor);
~idx && requestAdaptors.splice(idx, 1);
}
/**
*
* @param adaptor
*/
export function addApiResponseAdator(adaptor: ResponseAdaptor) {
responseAdaptors.push(adaptor);
return () => removeApiResponseAdaptor(adaptor);
}
/**
*
* @param adaptor
*/
export function removeApiResponseAdaptor(adaptor: ResponseAdaptor) {
const idx = responseAdaptors.findIndex(i => i === adaptor);
~idx && responseAdaptors.splice(idx, 1);
}
const isIE = !!(document as any).documentMode;
@ -488,25 +534,40 @@ export function wrapFetcher(
api = buildApi(api, data, options) as ApiObject;
(api as ApiObject).context = data;
const adaptors = requestAdaptors.concat();
if (api.requestAdaptor) {
debug('api', 'before requestAdaptor', api);
const originQuery = api.query;
const originQueryCopy = isPlainObject(api.query)
? cloneDeep(api.query)
: api.query;
api = (await api.requestAdaptor(api, data)) || api;
const adaptor = api.requestAdaptor;
adaptors.unshift(async (api: ApiObject, context) => {
const originQuery = api.query;
const originQueryCopy = isPlainObject(api.query)
? cloneDeep(api.query)
: api.query;
if (
api.query !== originQuery ||
(isPlainObject(api.query) && !isEqual(api.query, originQueryCopy))
) {
// 如果 api.data 有变化,且是 get 请求,那么需要重新构建 url
const idx = api.url.indexOf('?');
api.url = `${~idx ? api.url.substring(0, idx) : api.url}?${qsstringify(
api.query
)}`;
}
debug('api', 'after requestAdaptor', api);
debug('api', 'before requestAdaptor', api);
api = (await adaptor.call(api, api, context)) || api;
if (
api.query !== originQuery ||
(isPlainObject(api.query) && !isEqual(api.query, originQueryCopy))
) {
// 如果 api.data 有变化,且是 get 请求,那么需要重新构建 url
const idx = api.url.indexOf('?');
api.url = `${
~idx ? api.url.substring(0, idx) : api.url
}?${qsstringify(api.query)}`;
}
debug('api', 'after requestAdaptor', api);
return api;
});
}
// 执行所有的发送适配器
if (adaptors.length) {
api = await adaptors.reduce(async (api, fn) => {
let ret: any = await api;
ret = (await fn(ret, data)) || ret;
return ret as ApiObject;
}, Promise.resolve(api));
}
if (
@ -589,31 +650,50 @@ export function wrapFetcher(
return wrappedFetcher;
}
export function wrapAdaptor(
export async function wrapAdaptor(
promise: Promise<fetcherResult>,
api: ApiObject,
context: any
) {
const adaptor = api.adaptor;
return adaptor
? promise
.then(async response => {
debug('api', 'before adaptor data', (response as any).data);
let result = adaptor((response as any).data, response, api, context);
const adaptors = responseAdaptors.concat();
if (api.adaptor) {
const adaptor = api.adaptor;
adaptors.push(
async (
payload: object,
response: fetcherResult,
api: ApiObject,
context: any
) => {
debug('api', 'before adaptor data', (response as any).data);
let result = adaptor((response as any).data, response, api, context);
if (result?.then) {
result = await result;
}
if (result?.then) {
result = await result;
}
debug('api', 'after adaptor data', result);
debug('api', 'after adaptor data', result);
return result;
}
);
}
return {
...response,
data: result
};
})
.then(ret => responseAdaptor(ret, api))
: promise.then(ret => responseAdaptor(ret, api));
const response = await adaptors.reduce(async (promise, adaptor) => {
let response: any = await promise;
let result =
adaptor(response.data, response, api, context) ?? response.data;
if (result?.then) {
result = await result;
}
return {
...response,
data: result
} as fetcherResult;
}, promise);
return responseAdaptor(response, api);
}
/**

View File

@ -30,7 +30,7 @@ const external = id =>
export default [
{
input: ['./src/index.ts', './src/doc.ts'],
input: ['./src/index.ts'],
output: [
{
...settings,
@ -45,7 +45,7 @@ export default [
plugins: getPlugins('cjs')
},
{
input: ['./src/index.ts', './src/doc.ts'],
input: ['./src/index.ts'],
output: [
{
...settings,
@ -61,6 +61,23 @@ export default [
}
];
function transpileDynamicImportForCJS(options) {
return {
name: 'transpile-dynamic-import-for-cjs',
renderDynamicImport({format, targetModuleId}) {
if (format !== 'cjs') {
return null;
}
return {
left: 'Promise.resolve().then(function() {return new Promise(function(fullfill) {require([',
right:
', "tslib"], function(mod, tslib) {fullfill(tslib.__importStar(mod))})})})'
};
}
};
}
function getPlugins(format = 'esm') {
const typeScriptOptions = {
typescript: require('typescript'),
@ -83,6 +100,7 @@ function getPlugins(format = 'esm') {
};
return [
transpileDynamicImportForCJS(),
json(),
resolve({
jsnext: true,

View File

@ -89,26 +89,14 @@ async function main(...params: Array<any>) {
fs.writeFileSync(
outputFile,
`/**\n * 公式文档 请运行 \`npm run genDoc\` 自动生成\n */\nexport const doc: ${[
`{`,
` name: string;`,
` description: string;`,
` example: string;`,
` params: {`,
` type: string;`,
` name: string;`,
` description: string | null;`,
` }[];`,
` returns: {`,
` type: string;`,
` description: string | null;`,
` };`,
` namespace: string;`,
`}[]`
].join('\n')} = ${JSON.stringify(result, null, 2).replace(
`/**\n * 公式文档 请运行 \`npm run genDoc\` 自动生成\n */\n
import {bulkRegisterFunctionDoc} from './function';
bulkRegisterFunctionDoc(${JSON.stringify(result, null, 2).replace(
/\"(\w+)\"\:/g,
(_, key) => `${key}:`
)};`,
)});
`,
'utf8'
);
console.log(`公式文档生成 > ${outputFile}`);

View File

@ -1,21 +1,10 @@
/**
* `npm run genDoc`
*/
export const doc: {
name: string;
description: string;
example: string;
params: {
type: string;
name: string;
description: string | null;
}[];
returns: {
type: string;
description: string | null;
};
namespace: string;
}[] = [
import {bulkRegisterFunctionDoc} from './function';
bulkRegisterFunctionDoc([
{
name: 'IF',
description:
@ -1982,4 +1971,4 @@ export const doc: {
},
namespace: '其他'
}
];
]);

View File

@ -26,14 +26,14 @@ export class Evaluator {
contextStack: Array<(varname: string) => any> = [];
static defaultFilters: FilterMap = {};
static setDefaultFilters(filters: FilterMap) {
static extendDefaultFilters(filters: FilterMap) {
Evaluator.defaultFilters = {
...Evaluator.defaultFilters,
...filters
};
}
static defaultFunctions: FunctionMap = {};
static setDefaultFunctions(funtions: FunctionMap) {
static extendDefaultFunctions(funtions: FunctionMap) {
Evaluator.defaultFunctions = {
...Evaluator.defaultFunctions,
...funtions
@ -2385,6 +2385,10 @@ export class Evaluator {
}
}
// 兼容
(Evaluator as any).setDefaultFilters = Evaluator.extendDefaultFilters;
(Evaluator as any).setDefaultFunctions = Evaluator.extendDefaultFunctions;
export function getCookie(name: string) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);

View File

@ -33,12 +33,12 @@ export function registerFilter(
fn: (input: any, ...args: any[]) => any
): void {
filters[name] = fn;
Evaluator.setDefaultFilters(filters);
Evaluator.extendDefaultFilters(filters);
}
export function extendsFilters(value: FilterMap) {
Object.assign(filters, value);
Evaluator.setDefaultFilters(filters);
Evaluator.extendDefaultFilters(filters);
}
export function getFilters() {

View File

@ -1,17 +1,16 @@
import {Evaluator} from './evalutor';
import {FunctionMap, FunctionDocMap, FunctionDocItem} from './types';
export const functions: FunctionMap = {};
import {FunctionDocMap, FunctionDocItem} from './types';
export function registerFunction(
name: string,
fn: (input: any, ...args: any[]) => any
fn: (this: Evaluator, ...args: Array<any>) => any
): void {
functions[`fn${name}`] = fn;
Evaluator.setDefaultFunctions(functions);
Evaluator.extendDefaultFunctions({
[`fn${name}`]: fn
});
}
export let functionDocs: FunctionDocMap = {};
export const functionDocs: FunctionDocMap = {};
export function registerFunctionDoc(groupName: string, item: FunctionDocItem) {
if (functionDocs[groupName]) {
@ -20,3 +19,38 @@ export function registerFunctionDoc(groupName: string, item: FunctionDocItem) {
functionDocs[groupName] = [item];
}
}
export function bulkRegisterFunctionDoc(
fnDocs: {
name: string;
description: string;
example: string;
params: {
type: string;
name: string;
description: string | null;
}[];
returns: {
type: string;
description: string | null;
};
namespace: string;
}[]
) {
fnDocs.forEach(item => registerFunctionDoc(item.namespace || 'Others', item));
}
/**
*
* @param name
* @param fn
* @param fnInfo
*/
export function registerFormula(
name: string,
fn: (this: Evaluator, ...args: Array<any>) => any,
fnInfo?: FunctionDocItem
) {
registerFunction(name, fn);
fnInfo && registerFunctionDoc(fnInfo.namespace || 'Others', fnInfo);
}

View File

@ -3,7 +3,12 @@ import {AsyncEvaluator} from './evalutorForAsync';
import {parse} from './parser';
import {lexer} from './lexer';
import {registerFilter, filters, getFilters, extendsFilters} from './filter';
import {registerFunction, registerFunctionDoc, functionDocs} from './function';
import {
registerFunction,
registerFunctionDoc,
functionDocs,
registerFormula
} from './function';
import type {
FilterContext,
ASTNode,
@ -19,9 +24,9 @@ export {
filters,
getFilters,
registerFilter,
registerFormula,
registerFunction,
registerFunctionDoc,
functionDocs,
extendsFilters
};
@ -53,5 +58,14 @@ export async function evaluateForAsync(
return new AsyncEvaluator(data, options).evalute(ast);
}
Evaluator.setDefaultFilters(getFilters());
Evaluator.extendDefaultFilters(getFilters());
AsyncEvaluator.setDefaultFilters(getFilters());
export async function getFunctionsDoc() {
await import('./doc');
return Object.entries(functionDocs).map(([k, items]) => ({
groupName: k,
items
}));
}

View File

@ -5,13 +5,14 @@ export interface FilterMap {
}
export interface FunctionMap {
[propName: string]: (this: Evaluator, ast: Object, data: any) => any;
[propName: string]: (this: Evaluator, ...args: Array<any>) => any;
}
export interface FunctionDocItem {
name: string; // 函数名
example?: string; // 示例
description?: string; // 描述
namespace?: string;
[propName: string]: any;
}
export interface FunctionDocMap {

View File

@ -0,0 +1,3 @@
import Menu from './menu/index';
export default Menu;

View File

@ -0,0 +1,3 @@
import Table from './table/index';
export default Table;

View File

@ -15,15 +15,13 @@ import {
localeable,
LocaleProps
} from 'amis-core';
import type {FunctionDocMap} from 'amis-formula/lib/types';
import {editorFactory} from './plugin';
import FuncList from './FuncList';
import VariableList from './VariableList';
import {toast} from '../Toast';
import Switch from '../Switch';
import CodeEditor, {FuncGroup, FuncItem, VariableItem} from './CodeEditor';
import {functionDocs} from 'amis-formula';
import {getFunctionsDoc} from 'amis-formula';
import Transition, {
EXITED,
ENTERING,
@ -130,49 +128,13 @@ export class FormulaEditor extends React.Component<
unmounted: boolean = false;
editor = React.createRef<any>();
static buildDefaultFunctions(
doc: Array<{
namespace: string;
name: string;
[propName: string]: any;
}>
) {
const funcs: Array<FuncGroup> = [];
doc.forEach(item => {
const namespace = item.namespace || 'Others';
let exists = funcs.find(item => item.groupName === namespace);
if (!exists) {
exists = {
groupName: namespace,
items: []
};
funcs.push(exists);
}
exists.items.push(item);
});
return funcs;
}
static buildCustomFunctions(map: FunctionDocMap = {}) {
return Object.entries(map).map(([k, items]) => ({
groupName: k,
items
}));
}
static async buildFunctions(
functions?: Array<any>,
functionsFilter?: (functions: Array<FuncGroup>) => Array<FuncGroup>
): Promise<any> {
const {doc} = await import('amis-formula/lib/doc');
const builtInFunctions = await getFunctionsDoc();
const customFunctions = Array.isArray(functions) ? functions : [];
const functionList = [
...FormulaEditor.buildDefaultFunctions(doc),
...FormulaEditor.buildCustomFunctions(functionDocs),
...customFunctions
];
const functionList = [...builtInFunctions, ...customFunctions];
if (functionsFilter) {
return functionsFilter(functionList);

View File

@ -24,7 +24,6 @@ import Modal from '../Modal';
import PopUp from '../PopUp';
import FormulaInput from './Input';
import {FuncGroup, VariableItem} from './CodeEditor';
import {functionDocs} from 'amis-formula';
export const InputSchemaType = [
'text',

View File

@ -70,7 +70,7 @@ import TableSelection from './TableSelection';
import TreeSelection from './TreeSelection';
import AssociatedSelection from './AssociatedSelection';
import PullRefresh from './PullRefresh';
import Table from './table';
import Table from './Table';
import SchemaVariableListPicker from './schema-editor/SchemaVariableListPicker';
import SchemaVariableList from './schema-editor/SchemaVariableList';
import VariableList from './formula/VariableList';
@ -132,7 +132,7 @@ import InputTable from './InputTable';
import type {InputTableColumnProps} from './InputTable';
import ConfirmBox from './ConfirmBox';
import DndContainer from './DndContainer';
import Menu from './menu';
import Menu from './Menu';
import InputBoxWithSuggestion from './InputBoxWithSuggestion';
import {CodeMirrorEditor} from './CodeMirror';
import type CodeMirror from 'codemirror';

View File

@ -12,5 +12,11 @@ import './themes/default';
import type {SchemaEditorItemPlaceholder} from './components/schema-editor/Common';
import {schemaEditorItemPlaceholder} from './components/schema-editor/Common';
import withStore from './withStore';
import withRemoteConfig from './withRemoteConfig';
export {schemaEditorItemPlaceholder, SchemaEditorItemPlaceholder, withStore};
export {
schemaEditorItemPlaceholder,
SchemaEditorItemPlaceholder,
withStore,
withRemoteConfig
};

View File

@ -0,0 +1,2 @@
import {withRemoteConfig} from './components/WithRemoteConfig';
export default withRemoteConfig;

View File

@ -3,7 +3,7 @@ import * as renderer from 'react-test-renderer';
import {fireEvent, render, waitFor} from '@testing-library/react';
import '../../../src';
import {render as amisRender} from '../../../src';
import {makeEnv} from '../../helper';
import {makeEnv, replaceReactAriaIds, wait} from '../../helper';
test('doAction:crud reload', async () => {
const notify = jest.fn();
@ -243,6 +243,8 @@ test('doAction:crud reload', async () => {
);
});
await wait(500);
replaceReactAriaIds(container);
expect(container).toMatchSnapshot();
});
@ -407,6 +409,7 @@ test('doAction:crud reload with data1', async () => {
);
});
replaceReactAriaIds(container);
expect(container).toMatchSnapshot();
});
@ -574,5 +577,6 @@ test('doAction:crud reload with data2', async () => {
);
});
replaceReactAriaIds(container);
expect(container).toMatchSnapshot();
});

View File

@ -112,6 +112,13 @@ export function replaceReactAriaIds(container: HTMLElement) {
}
});
});
container.querySelectorAll('[data-id]').forEach(el => {
const val = el.getAttribute('data-id');
if (typeof val === 'string' && /^[a-z0-9]{12}$/.test(val)) {
el.removeAttribute('data-id');
}
});
}
// Mock IntersectionObserver

View File

@ -24,7 +24,6 @@ import {
import '../../../src';
import {render as amisRender, clearStoresCache} from '../../../src';
import {makeEnv, replaceReactAriaIds, wait} from '../../helper';
import {Select} from 'packages/amis-ui/lib/components/Select';
afterEach(() => {
cleanup();

View File

@ -226,7 +226,8 @@
"\\.svg\\.js$": "<rootDir>/../../__mocks__/svgJsMock.js",
"^amis\\-core$": "<rootDir>/../amis-core/src/index.tsx",
"^amis\\-ui$": "<rootDir>/../amis-ui/src/index.tsx",
"^amis\\-ui/lib/(.*)$": "<rootDir>/../amis-ui/src/$1"
"^amis\\-ui/lib/(.*)$": "<rootDir>/../amis-ui/src/$1",
"^amis\\-formula$": "<rootDir>/../amis-formula/src/index.ts"
},
"setupFilesAfterEnv": [
"<rootDir>/../amis-core/__tests__/jest.setup.js"

View File

@ -227,7 +227,7 @@ function getPlugins(format = 'esm') {
return `amis-ui/lib/components/Toast`;
} else if ('NotFound' === name) {
return `amis-ui/lib/components/404`;
} else if ('withStore' === name) {
} else if (['withStore', 'withRemoteConfig'].includes(name)) {
return `amis-ui/lib/${name}`;
} /* else if (name[0].toUpperCase() === name[0]) {
return `amis-ui/lib/components/${name}`;

View File

@ -8,6 +8,7 @@
export * from 'amis-core';
export * from 'amis-ui';
import './minimal';
import {registerFilter, registerFormula} from 'amis-formula';
import type {
BaseSchema,
@ -44,5 +45,7 @@ export {
SchemaExpression,
Action,
SchemaType,
EditorAvailableLanguages
EditorAvailableLanguages,
registerFilter,
registerFormula
};