Merge branch 'master' into fix-service

This commit is contained in:
lvxiaojiao 2023-07-21 11:02:21 +08:00
commit a0a1f971f4
19 changed files with 718 additions and 107 deletions

View File

@ -98,7 +98,13 @@ import Overlay from './components/Overlay';
import PopOver from './components/PopOver'; import PopOver from './components/PopOver';
import {FormRenderer} from './renderers/Form'; import {FormRenderer} from './renderers/Form';
import type {FormHorizontal, FormSchemaBase} from './renderers/Form'; import type {FormHorizontal, FormSchemaBase} from './renderers/Form';
import {enableDebug, promisify, replaceText, wrapFetcher} from './utils/index'; import {
enableDebug,
disableDebug,
promisify,
replaceText,
wrapFetcher
} from './utils/index';
import type {OnEventProps} from './utils/index'; import type {OnEventProps} from './utils/index';
import {valueMap as styleMap} from './utils/style-helper'; import {valueMap as styleMap} from './utils/style-helper';
import {RENDERER_TRANSMISSION_OMIT_PROPS} from './SchemaRenderer'; import {RENDERER_TRANSMISSION_OMIT_PROPS} from './SchemaRenderer';
@ -195,7 +201,9 @@ export {
OnEventProps, OnEventProps,
FormSchemaBase, FormSchemaBase,
filterTarget, filterTarget,
CustomStyle CustomStyle,
enableDebug,
disableDebug
}; };
export function render( export function render(
@ -257,13 +265,6 @@ function AMISRenderer({
translate translate
} as any; } as any;
if (options.enableAMISDebug) {
// 因为里面还有 render
setTimeout(() => {
enableDebug();
}, 10);
}
store = RendererStore.create({}, options); store = RendererStore.create({}, options);
stores[options.session || 'global'] = store; stores[options.session || 'global'] = store;
} else { } else {
@ -291,6 +292,11 @@ function AMISRenderer({
} }
env.theme = getTheme(theme); env.theme = getTheme(theme);
React.useEffect(() => {
env.enableAMISDebug ? enableDebug() : disableDebug();
return () => env.enableAMISDebug || disableDebug();
}, [env.enableAMISDebug]);
if (props.locale !== undefined) { if (props.locale !== undefined) {
env.translate = translate; env.translate = translate;
env.locale = locale; env.locale = locale;

View File

@ -223,7 +223,10 @@ export interface ApiObject extends BaseApiObject {
api: ApiObject, api: ApiObject,
context: any context: any
) => any; ) => any;
requestAdaptor?: (api: ApiObject, context: any) => ApiObject; requestAdaptor?: (
api: ApiObject,
context: any
) => ApiObject | Promise<ApiObject>;
/** 是否过滤为空字符串的 query 参数 */ /** 是否过滤为空字符串的 query 参数 */
filterEmptyQuery?: boolean; filterEmptyQuery?: boolean;
} }

View File

@ -76,7 +76,7 @@ export function buildApi(
} }
if (api.requestAdaptor && typeof api.requestAdaptor === 'string') { if (api.requestAdaptor && typeof api.requestAdaptor === 'string') {
api.requestAdaptor = str2function( api.requestAdaptor = str2AsyncFunction(
api.requestAdaptor, api.requestAdaptor,
'api', 'api',
'context' 'context'
@ -84,7 +84,7 @@ export function buildApi(
} }
if (api.adaptor && typeof api.adaptor === 'string') { if (api.adaptor && typeof api.adaptor === 'string') {
api.adaptor = str2function( api.adaptor = str2AsyncFunction(
api.adaptor, api.adaptor,
'payload', 'payload',
'response', 'response',
@ -464,12 +464,16 @@ export function wrapFetcher(
return fn as any; return fn as any;
} }
const wrappedFetcher = function (api: Api, data: object, options?: object) { const wrappedFetcher = async function (
api: Api,
data: object,
options?: object
) {
api = buildApi(api, data, options) as ApiObject; api = buildApi(api, data, options) as ApiObject;
if (api.requestAdaptor) { if (api.requestAdaptor) {
debug('api', 'before requestAdaptor', api); debug('api', 'before requestAdaptor', api);
api = api.requestAdaptor(api, data) || api; api = (await api.requestAdaptor(api, data)) || api;
debug('api', 'after requestAdaptor', api); debug('api', 'after requestAdaptor', api);
} }

View File

@ -2,9 +2,10 @@
* amis amis * amis amis
*/ */
import React, {Component, useEffect, useRef, useState} from 'react'; import React, {Component, useEffect, useRef, useState, version} from 'react';
import cx from 'classnames'; import cx from 'classnames';
import {findDOMNode, render} from 'react-dom'; import {findDOMNode, render, unmountComponentAtNode} from 'react-dom';
import {createRoot} from 'react-dom/client';
import {autorun, observable} from 'mobx'; import {autorun, observable} from 'mobx';
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import {uuidv4} from './helper'; import {uuidv4} from './helper';
@ -84,16 +85,18 @@ const LogView = observer(({store}: {store: AMISDebugStore}) => {
[{log.cat}] {log.msg} [{log.cat}] {log.msg}
</div> </div>
{log.ext ? ( {log.ext ? (
<JsonView <React.Suspense fallback={<div>Loading...</div>}>
name={null} <JsonView
theme="monokai" name={null}
src={JSON.parse(log.ext)} theme="monokai"
collapsed={true} src={JSON.parse(log.ext)}
enableClipboard={false} collapsed={true}
displayDataTypes={false} enableClipboard={false}
collapseStringsAfterLength={ellipsisThreshold} displayDataTypes={false}
iconStyle="square" collapseStringsAfterLength={ellipsisThreshold}
/> iconStyle="square"
/>
</React.Suspense>
) : null} ) : null}
</div> </div>
); );
@ -126,16 +129,18 @@ const AMISDebug = observer(({store}: {store: AMISDebugStore}) => {
stackDataView.push( stackDataView.push(
<div key={`data-${level}`}> <div key={`data-${level}`}>
<h3>Data Level-{level}</h3> <h3>Data Level-{level}</h3>
<JsonView <React.Suspense fallback={<div>Loading...</div>}>
key={`dataview-${stack}`} <JsonView
name={null} key={`dataview-${stack}`}
theme="monokai" name={null}
src={stack} theme="monokai"
collapsed={level === 0 ? false : true} src={stack}
enableClipboard={false} collapsed={level === 0 ? false : true}
displayDataTypes={false} enableClipboard={false}
iconStyle="square" displayDataTypes={false}
/> iconStyle="square"
/>
</React.Suspense>
</div> </div>
); );
level += 1; level += 1;
@ -319,7 +324,7 @@ function handleMouseclick(e: MouseEvent) {
} }
const dom = e.target as HTMLElement; const dom = e.target as HTMLElement;
const target = dom.closest(`[data-debug-id]`); const target = dom.closest(`[data-debug-id]`);
if (target) { if (target && !target.closest('.AMISDebug')) {
store.activeId = target.getAttribute('data-debug-id')!; store.activeId = target.getAttribute('data-debug-id')!;
store.tab = 'inspect'; store.tab = 'inspect';
} }
@ -366,6 +371,7 @@ autorun(() => {
// 页面中只能有一个实例 // 页面中只能有一个实例
let isEnabled = false; let isEnabled = false;
let unmount: () => void;
export function enableDebug() { export function enableDebug() {
if (isEnabled) { if (isEnabled) {
@ -376,7 +382,21 @@ export function enableDebug() {
const amisDebugElement = document.createElement('div'); const amisDebugElement = document.createElement('div');
document.body.appendChild(amisDebugElement); document.body.appendChild(amisDebugElement);
const element = <AMISDebug store={store} />; const element = <AMISDebug store={store} />;
render(element, amisDebugElement);
if (parseInt(version.split('.')[0], 10) >= 18) {
const root = createRoot(amisDebugElement);
root.render(element);
unmount = () => {
root.unmount();
document.body.removeChild(amisDebugElement);
};
} else {
render(element, amisDebugElement);
unmount = () => {
unmountComponentAtNode(amisDebugElement);
document.body.removeChild(amisDebugElement);
};
}
document.body.appendChild(amisHoverBox); document.body.appendChild(amisHoverBox);
document.body.appendChild(amisActiveBox); document.body.appendChild(amisActiveBox);
@ -384,6 +404,18 @@ export function enableDebug() {
document.addEventListener('click', handleMouseclick); document.addEventListener('click', handleMouseclick);
} }
export function disableDebug() {
if (!isEnabled) {
return;
}
isEnabled = false;
unmount?.();
document.body.removeChild(amisHoverBox);
document.body.removeChild(amisActiveBox);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('click', handleMouseclick);
}
interface DebugWrapperProps { interface DebugWrapperProps {
renderer: any; renderer: any;
children?: React.ReactNode; children?: React.ReactNode;

View File

@ -40,6 +40,7 @@
&-tab { &-tab {
overflow: hidden; overflow: hidden;
border-bottom: 1px solid #3d3d3d;
} }
&-tab > button { &-tab > button {
@ -90,6 +91,7 @@
&-content { &-content {
pointer-events: all; pointer-events: all;
display: none; display: none;
height: 100%;
} }
&-resize { &-resize {
@ -122,7 +124,7 @@
&.is-expanded { &.is-expanded {
width: 420px; width: 420px;
overflow: auto;
background: #272821; background: #272821;
color: #cccccc; color: #cccccc;
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
@ -168,6 +170,37 @@
padding: var(--gap-sm); padding: var(--gap-sm);
} }
&-log,
&-inspect {
height: 100%;
overflow: auto;
// 火狐浏览器
scrollbar-width: thin;
scrollbar-color: #6b6b6b #2b2b2b;
&::-webkit-scrollbar {
position: relative;
z-index: 10;
background-color: #2c2c2c;
width: 16px;
height: 16px;
border-left: 1px solid #3d3d3d;
// border-top: 1px solid #3d3d3d;
}
&::-webkit-scrollbar-thumb {
background: #6b6b6b;
background-clip: content-box;
border: 4px solid transparent;
border-radius: 500px;
&:hover {
background: #939393;
background-clip: content-box;
}
}
}
&-logLine { &-logLine {
overflow-x: hidden; overflow-x: hidden;
} }

View File

@ -89,11 +89,20 @@
} }
&-body { &-body {
padding: var(--drawer-content-paddingTop) var(--drawer-content-paddingRight) padding: 0 var(--drawer-content-paddingRight)
var(--drawer-content-paddingBottom) var(--drawer-content-paddingLeft); var(--drawer-content-paddingBottom) var(--drawer-content-paddingLeft);
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
overflow: auto; overflow: auto;
// 因为如果成员里面有 position:sticky 的内容
// padding 会导致位置不正确
// 所以改成这种写法
&:before {
content: '';
display: block;
height: var(--drawer-content-paddingTop);
}
} }
&-footer { &-footer {

View File

@ -350,6 +350,7 @@
&-submenu-title { &-submenu-title {
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
.#{$ns}Nav-Menu-item-wrap { .#{$ns}Nav-Menu-item-wrap {

View File

@ -3,7 +3,7 @@
* @author fex * @author fex
*/ */
import React from 'react'; import React, {version} from 'react';
import {render} from 'react-dom'; import {render} from 'react-dom';
import Modal from './Modal'; import Modal from './Modal';
import Button from './Button'; import Button from './Button';
@ -11,6 +11,7 @@ import {ClassNamesFn, themeable, ThemeProps} from 'amis-core';
import {LocaleProps, localeable} from 'amis-core'; import {LocaleProps, localeable} from 'amis-core';
import Html from './Html'; import Html from './Html';
import type {PlainObject} from 'amis-core'; import type {PlainObject} from 'amis-core';
import {createRoot} from 'react-dom/client';
export interface AlertProps extends ThemeProps, LocaleProps { export interface AlertProps extends ThemeProps, LocaleProps {
container?: any; container?: any;
confirmText?: string; confirmText?: string;
@ -52,13 +53,21 @@ export interface AlertState {
export class Alert extends React.Component<AlertProps, AlertState> { export class Alert extends React.Component<AlertProps, AlertState> {
static instance: any = null; static instance: any = null;
static getInstance() { static async getInstance() {
if (!Alert.instance) { if (!Alert.instance) {
console.warn('Alert 组件应该没有被渲染,所以隐性的渲染到 body 了'); console.warn('Alert 组件应该没有被渲染,所以隐性的渲染到 body 了');
const container = document.body; const container = document.body;
const div = document.createElement('div'); const div = document.createElement('div');
container.appendChild(div); container.appendChild(div);
render(<FinnalAlert />, div);
if (parseInt(version.split('.')[0], 10) >= 18) {
const root = createRoot(div);
await new Promise<void>(resolve =>
root.render(<FinnalAlert ref={() => resolve()} />)
);
} else {
render(<FinnalAlert />, div);
}
} }
return Alert.instance; return Alert.instance;
@ -346,23 +355,35 @@ function renderForm(
return renderSchemaFn?.(controls, value, callback, scopeRef, theme); return renderSchemaFn?.(controls, value, callback, scopeRef, theme);
} }
export const alert: (content: string, title?: string) => void = ( export const alert: (content: string, title?: string) => Promise<void> = async (
content, content,
title title
) => Alert.getInstance().alert(content, title); ) => {
const instance = await Alert.getInstance();
return instance.alert(content, title);
};
export const confirm: ( export const confirm: (
content: string | React.ReactNode, content: string | React.ReactNode,
title?: string, title?: string,
optionsOrCofnrimText?: string | ConfirmOptions, optionsOrCofnrimText?: string | ConfirmOptions,
cancelText?: string cancelText?: string
) => Promise<any> = (content, title, optionsOrCofnrimText, cancelText) => ) => Promise<any> = async (
Alert.getInstance().confirm(content, title, optionsOrCofnrimText, cancelText); content,
title,
optionsOrCofnrimText,
cancelText
) => {
const instance = await Alert.getInstance();
return instance.confirm(content, title, optionsOrCofnrimText, cancelText);
};
export const prompt: ( export const prompt: (
controls: any, controls: any,
defaultvalue?: any, defaultvalue?: any,
title?: string, title?: string,
confirmText?: string confirmText?: string
) => Promise<any> = (controls, defaultvalue, title, confirmText) => ) => Promise<any> = async (controls, defaultvalue, title, confirmText) => {
Alert.getInstance().prompt(controls, defaultvalue, title, confirmText); const instance = await Alert.getInstance();
return instance.prompt(controls, defaultvalue, title, confirmText);
};
export const FinnalAlert = themeable(localeable(Alert)); export const FinnalAlert = themeable(localeable(Alert));
export default FinnalAlert; export default FinnalAlert;

View File

@ -1,5 +1,5 @@
import {ClassNamesFn, themeable} from 'amis-core'; import {ClassNamesFn, themeable} from 'amis-core';
import React from 'react'; import React, {version} from 'react';
import {render} from 'react-dom'; import {render} from 'react-dom';
import {autobind, calculatePosition} from 'amis-core'; import {autobind, calculatePosition} from 'amis-core';
import Transition, { import Transition, {
@ -7,6 +7,7 @@ import Transition, {
ENTERING, ENTERING,
EXITING EXITING
} from 'react-transition-group/Transition'; } from 'react-transition-group/Transition';
import {createRoot} from 'react-dom/client';
const fadeStyles: { const fadeStyles: {
[propName: string]: string; [propName: string]: string;
} = { } = {
@ -49,12 +50,20 @@ export class ContextMenu extends React.Component<
ContextMenuState ContextMenuState
> { > {
static instance: any = null; static instance: any = null;
static getInstance() { static async getInstance() {
if (!ContextMenu.instance) { if (!ContextMenu.instance) {
const container = document.body; const container = document.body;
const div = document.createElement('div'); const div = document.createElement('div');
container.appendChild(div); container.appendChild(div);
render(<ThemedContextMenu />, div);
if (parseInt(version.split('.')[0], 10) >= 18) {
const root = createRoot(div);
await new Promise<void>(resolve =>
root.render(<ThemedContextMenu ref={() => resolve()} />)
);
} else {
render(<ThemedContextMenu />, div);
}
} }
return ContextMenu.instance; return ContextMenu.instance;
@ -309,5 +318,7 @@ export function openContextMenus(
menus: Array<MenuItem | MenuDivider>, menus: Array<MenuItem | MenuDivider>,
onClose?: () => void onClose?: () => void
) { ) {
return ContextMenu.getInstance().openContextMenus(info, menus, onClose); return ContextMenu.getInstance().then(instance =>
instance.openContextMenus(info, menus, onClose)
);
} }

View File

@ -75,7 +75,7 @@ exports[`doAction:service reload 1`] = `
placeholder="" placeholder=""
size="10" size="10"
type="text" type="text"
value="Amis Renderer" value="amis"
/> />
</div> </div>
</div> </div>
@ -321,7 +321,7 @@ exports[`doAction:service reload 2`] = `
placeholder="" placeholder=""
size="10" size="10"
type="text" type="text"
value="Amis Renderer" value="amis"
/> />
</div> </div>
</div> </div>

View File

@ -342,7 +342,7 @@ test('Renderers:Action tooltip', async () => {
// }); // });
// 14. confirmText // 14. confirmText
test('Renderers:Action with confirmText & actionType ajax', () => { test('Renderers:Action with confirmText & actionType ajax', async () => {
const fetcher = jest.fn().mockImplementation(() => const fetcher = jest.fn().mockImplementation(() =>
Promise.resolve({ Promise.resolve({
data: { data: {
@ -372,7 +372,7 @@ test('Renderers:Action with confirmText & actionType ajax', () => {
) )
); );
fireEvent.click(container.querySelector('.cxd-Button')); fireEvent.click(container.querySelector('.cxd-Button'));
wait(500); await wait(500);
expect(baseElement).toMatchSnapshot(); expect(baseElement).toMatchSnapshot();
expect(baseElement.querySelector('.cxd-Modal-content')!).toHaveTextContent( expect(baseElement.querySelector('.cxd-Modal-content')!).toHaveTextContent(
@ -380,14 +380,16 @@ test('Renderers:Action with confirmText & actionType ajax', () => {
); );
fireEvent.click(getByText('取消')); fireEvent.click(getByText('取消'));
wait(500); await wait(500);
expect(fetcher).not.toBeCalled(); expect(fetcher).not.toBeCalled();
// fireEvent.click(container.querySelector('.cxd-Button')); fireEvent.click(container.querySelector('.cxd-Button'));
// wait(500); await wait(500);
// fireEvent.click(getByText('确认')); fireEvent.click(getByText('确认'));
// fetcher 不生效
// expect(fetcher).toBeCalled(); await wait(200);
// fetcher 该被执行了
expect(fetcher).toBeCalled();
}); });
// 15.Action 作为容器组件 // 15.Action 作为容器组件

View File

@ -136,13 +136,15 @@ test('Renderer: input-table with default value column', async () => {
await wait(200); await wait(200);
expect(onSubmitCallbackFn).toHaveBeenCalledTimes(1); expect(onSubmitCallbackFn).toHaveBeenCalledTimes(1);
expect(onSubmitCallbackFn.mock.calls[0][0]).toEqual({ expect(onSubmitCallbackFn.mock.calls[0][0]).toEqual(
table: [ expect.objectContaining({
{a: 'a1', b: 'b1', c: 'a1'}, table: [
{a: 'a2', b: 'b2', c: 'a2'}, {a: 'a1', b: 'b1', c: 'a1'},
{a: 'a3', b: 'b3', c: 'a3'} {a: 'a2', b: 'b2', c: 'a2'},
] {a: 'a3', b: 'b3', c: 'a3'}
}); ]
})
);
}, 10000); }, 10000);
test('Renderer:input table add', async () => { test('Renderer:input table add', async () => {

View File

@ -517,3 +517,181 @@ test('Renderer:Nav with itemActions', async () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
expect(getByText('编辑')).toBeInTheDocument(); expect(getByText('编辑')).toBeInTheDocument();
}); });
// 8.各种图标展示
test('Renderer:Nav with icons', async () => {
const {container} = render(
amisRender(
{
type: 'page',
body: {
type: 'nav',
stacked: true,
links: [
{
label: 'Nav 1',
to: '?cat=1',
value: '1',
icon: 'fa fa-user',
__id: 1
},
{
label: 'Nav 2',
__id: 2,
unfolded: true,
children: [
{
__id: 2.1,
label: 'Nav 2-1',
icon: [
{
icon: 'star',
position: 'before'
},
{
icon: 'search',
position: 'before'
},
{
icon: 'https://suda.cdn.bcebos.com/images%2F2021-01%2Fdiamond.svg',
position: 'after'
}
],
children: [
{
label: 'Nav 2-1-1',
to: '?cat=2-1',
value: '2-1',
__id: 2.11
}
]
}
]
}
]
}
},
{},
makeEnv({})
)
);
expect(container).toMatchSnapshot();
expect(container.querySelectorAll('.fa-user').length).toBe(1);
expect(container.querySelectorAll('[icon=search]').length).toBe(1);
expect(container.querySelectorAll('img').length).toBe(1);
});
// 9.Nav在Dialog里
test('Renderer:Nav with Dialog', async () => {
const {container, getByText} = render(
amisRender(
{
type: 'page',
body: {
type: 'button',
label: '点击弹框',
actionType: 'dialog',
dialog: {
title: '弹框',
body: [
{
type: 'nav',
stacked: true,
className: 'w-md',
draggable: true,
saveOrderApi: '/api/options/nav',
source: '/api/options/nav?parentId=${value}',
itemActions: [
{
type: 'icon',
icon: 'cloud',
visibleOn: "this.to === '?cat=1'"
},
{
type: 'dropdown-button',
level: 'link',
icon: 'fa fa-ellipsis-h',
hideCaret: true,
buttons: [
{
type: 'button',
label: '编辑'
},
{
type: 'button',
label: '删除'
}
]
}
],
links: [
{
label: 'Nav 1',
to: '?cat=1',
value: '1',
icon: 'fa fa-user',
__id: 1
},
{
label: 'Nav 2',
__id: 2,
unfolded: true,
children: [
{
__id: 2.1,
label: 'Nav 2-1',
children: [
{
label: 'Nav 2-1-1',
to: '?cat=2-1',
value: '2-1',
__id: 2.11
}
]
},
{
label: 'Nav 2-2',
to: '?cat=2-2',
value: '2-2',
__id: 2.2
}
]
},
{
label: 'Nav 3',
to: '?cat=3',
value: '3',
defer: true,
__id: 3
}
]
}
]
}
}
},
{},
makeEnv({
getModalContainer: () => container
})
)
);
expect(container).toMatchSnapshot();
fireEvent.click(getByText('点击弹框'));
await waitFor(() => {
expect(container.querySelector('[role="dialog"]')).toBeInTheDocument();
});
fireEvent.click(
container.querySelector(
'[role="dialog"] .cxd-Nav-Menu-item-extra .cxd-Button'
)!
);
await waitFor(() => {
expect(
container.querySelector('[role="dialog"] .cxd-PopOver')
).toBeInTheDocument();
});
});

View File

@ -208,6 +208,228 @@ exports[`Renderer:Nav 1`] = `
</div> </div>
`; `;
exports[`Renderer:Nav with Dialog 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
role="page-body"
>
<button
class="cxd-Button cxd-Button--default cxd-Button--size-default"
type="button"
>
<span>
点击弹框
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Renderer:Nav with icons 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
role="page-body"
>
<div
class="cxd-Nav"
>
<ul
class="cxd-Nav-Menu cxd-Nav-Menu-root cxd-Nav-Menu-inline cxd-Nav-Menu-ltr cxd-Nav-Menu-light cxd-Nav-Menu-expand-before"
data-menu-list="true"
dir="ltr"
role="menu"
tabindex="0"
>
<ul
class="cxd-Nav-Menu-item-tooltip-wrap"
style="order: 0;"
>
<li
aria-disabled="false"
class="cxd-Nav-Menu-item"
data-menu-id="rc-menu-uuid-test-1"
role="menuitem"
style="padding-left: 16px;"
tabindex="-1"
>
<div
class="cxd-Nav-Menu-item-wrap"
>
<a
class="cxd-Nav-Menu-item-link"
data-depth="1"
data-id="1"
title="Nav 1"
>
<i
class="cxd-Nav-Menu-item-icon"
>
<i
class="fa fa-user fa fa-user"
/>
</i>
<span
class="cxd-Nav-Menu-item-label"
title="Nav 1"
>
Nav 1
</span>
</a>
</div>
</li>
</ul>
<li
class="cxd-Nav-Menu-submenu cxd-Nav-Menu-submenu-inline cxd-Nav-Menu-submenu cxd-Nav-Menu-submenu-open"
role="none"
>
<div
aria-controls="rc-menu-uuid-test-2-popup"
aria-expanded="true"
aria-haspopup="true"
class="cxd-Nav-Menu-submenu-title"
data-menu-id="rc-menu-uuid-test-2"
role="menuitem"
style="padding-left: 16px;"
tabindex="-1"
>
<div
class="cxd-Nav-Menu-item-wrap"
>
<a
class="cxd-Nav-Menu-item-link"
data-depth="1"
data-id="2"
>
<span
class="cxd-Nav-Menu-item-label cxd-Nav-Menu-item-label-subTitle"
title="Nav 2"
>
Nav 2
</span>
</a>
</div>
<span
class="cxd-Nav-Menu-submenu-arrow"
>
<icon-mock
classname="icon icon-right-arrow-bold"
icon="right-arrow-bold"
/>
</span>
</div>
<ul
class="cxd-Nav-Menu cxd-Nav-Menu-sub cxd-Nav-Menu-inline"
data-menu-list="true"
id="rc-menu-uuid-test-2-popup"
role="menu"
>
<li
class="cxd-Nav-Menu-submenu cxd-Nav-Menu-submenu-inline cxd-Nav-Menu-submenu"
role="none"
>
<div
aria-controls="rc-menu-uuid-test-3-popup"
aria-expanded="false"
aria-haspopup="true"
class="cxd-Nav-Menu-submenu-title"
data-menu-id="rc-menu-uuid-test-3"
role="menuitem"
style="padding-left: 32px;"
tabindex="-1"
>
<div
class="cxd-Nav-Menu-item-wrap"
>
<a
class="cxd-Nav-Menu-item-link"
data-depth="2"
data-id="2.1"
>
<i
class="cxd-Nav-Menu-item-icon"
>
<icon-mock
classname="icon-star"
icon="star"
/>
<icon-mock
classname="icon-search"
icon="search"
/>
</i>
<span
class="cxd-Nav-Menu-item-label cxd-Nav-Menu-item-label-subTitle"
title="Nav 2-1"
>
Nav 2-1
</span>
<i
class="cxd-Nav-Menu-item-icon-after"
>
<img
class="cxd-Icon"
src="https://suda.cdn.bcebos.com/images%2F2021-01%2Fdiamond.svg"
/>
</i>
</a>
</div>
<span
class="cxd-Nav-Menu-submenu-arrow"
>
<icon-mock
classname="icon icon-right-arrow-bold"
icon="right-arrow-bold"
/>
</span>
</div>
</li>
</ul>
</li>
</ul>
<div
aria-hidden="true"
style="display: none;"
>
<ul
class="cxd-Nav-Menu-item-tooltip-wrap"
style="order: 0;"
/>
<ul
class="cxd-Nav-Menu-item-tooltip-wrap"
style="order: 0;"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Renderer:Nav with itemActions 1`] = ` exports[`Renderer:Nav with itemActions 1`] = `
<div> <div>
<div <div

View File

@ -1,6 +1,6 @@
import {render as amisRender} from '../../src'; import {render as amisRender} from '../../src';
import {wait, makeEnv} from '../helper'; import {wait, makeEnv} from '../helper';
import {render, fireEvent, cleanup} from '@testing-library/react'; import {render, fireEvent, cleanup, waitFor} from '@testing-library/react';
import {buildApi, isApiOutdated, isValidApi} from 'amis-core'; import {buildApi, isApiOutdated, isValidApi} from 'amis-core';
test('api:buildApi', () => { test('api:buildApi', () => {
@ -358,3 +358,74 @@ test('api:isvalidapi', () => {
) )
).toBeTruthy(); ).toBeTruthy();
}); });
test('api:requestAdaptor', async () => {
const notify = jest.fn();
const fetcher = jest.fn().mockImplementation(() =>
Promise.resolve({
data: {
status: 0,
msg: 'ok',
data: {
id: 1
}
}
})
);
const requestAdaptor = jest.fn().mockImplementation(api => {
return Promise.resolve({
...api,
data: {
...api.data,
email: 'appended@test.com'
}
});
});
const {container, getByText} = render(
amisRender(
{
type: 'page',
body: [
{
type: 'form',
id: 'form_submit',
submitText: '提交表单',
api: {
method: 'post',
url: '/api/mock2/form/saveForm',
requestAdaptor: requestAdaptor
},
body: [
{
type: 'input-text',
name: 'name',
label: '姓名:',
value: 'fex'
}
]
}
]
},
{},
makeEnv({
notify,
fetcher
})
)
);
await waitFor(() => {
expect(getByText('提交表单')).toBeInTheDocument();
});
fireEvent.click(getByText(/提交表单/));
await wait(300);
expect(requestAdaptor).toHaveBeenCalled();
expect(fetcher).toHaveBeenCalled();
expect(fetcher.mock.calls[0][0].data).toMatchObject({
name: 'fex',
email: 'appended@test.com'
});
});

View File

@ -465,6 +465,10 @@ export default class Dialog extends React.Component<DialogProps> {
syncLocation: false // 弹框中的 crud 一般不需要同步地址栏 syncLocation: false // 弹框中的 crud 一般不需要同步地址栏
}; };
if (this.props.size === 'full') {
subProps.affixOffsetTop = 0;
}
if (!(body as Schema).type) { if (!(body as Schema).type) {
return render(`body${key ? `/${key}` : ''}`, body, subProps); return render(`body${key ? `/${key}` : ''}`, body, subProps);
} }

View File

@ -465,7 +465,8 @@ export default class Drawer extends React.Component<DrawerProps> {
onInit: this.handleFormInit, onInit: this.handleFormInit,
onSaved: this.handleFormSaved, onSaved: this.handleFormSaved,
onActionSensor: this.handleActionSensor, onActionSensor: this.handleActionSensor,
syncLocation: false syncLocation: false,
affixOffsetTop: 0
}; };
if (schema.type === 'form') { if (schema.type === 'form') {

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import {findDOMNode} from 'react-dom'; import {findDOMNode} from 'react-dom';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import isString from 'lodash/isString';
import { import {
Renderer, Renderer,
RendererEnv, RendererEnv,
@ -27,7 +28,7 @@ import {
} from 'amis-core'; } from 'amis-core';
import {isEffectiveApi} from 'amis-core'; import {isEffectiveApi} from 'amis-core';
import {themeable, ThemeProps} from 'amis-core'; import {themeable, ThemeProps} from 'amis-core';
import {Icon, getIcon, SpinnerExtraProps} from 'amis-ui'; import {Icon, SpinnerExtraProps} from 'amis-ui';
import {BadgeObject} from 'amis-ui'; import {BadgeObject} from 'amis-ui';
import {RemoteOptionsProps, withRemoteConfig} from 'amis-ui'; import {RemoteOptionsProps, withRemoteConfig} from 'amis-ui';
import {Spinner, Menu} from 'amis-ui'; import {Spinner, Menu} from 'amis-ui';
@ -143,11 +144,6 @@ export interface NavOverflow {
* *
*/ */
style?: React.CSSProperties; style?: React.CSSProperties;
/**
* DOM挂载点
*/
popOverContainer?: any;
} }
/** /**
@ -319,6 +315,10 @@ export interface NavigationProps
data: Object; data: Object;
reload?: any; reload?: any;
overflow?: NavOverflow; overflow?: NavOverflow;
/**
* DOM挂载点
*/
popOverContainer?: () => HTMLElement;
} }
export interface IDropInfo { export interface IDropInfo {
@ -550,6 +550,8 @@ export class Navigation extends React.Component<
mode, mode,
itemActions, itemActions,
render, render,
popOverContainer,
env,
classnames: cx, classnames: cx,
data data
} = this.props; } = this.props;
@ -562,32 +564,28 @@ export class Navigation extends React.Component<
} }
return links.map((link: Link) => { return links.map((link: Link) => {
let beforeIcon = null; const beforeIcon: Array<any> = [];
let afterIcon = null; const afterIcon: Array<any> = [];
if (Array.isArray(link.icon)) {
beforeIcon = link.icon link.icon &&
.filter(item => item.position === 'before') (Array.isArray(link.icon) ? link.icon : [link.icon]).forEach(
.map(item => { (item, i) => {
if (React.isValidElement(item)) { if (React.isValidElement(item)) {
return item; beforeIcon.push(item);
} else if (isString(item)) {
beforeIcon.push(<Icon key={`icon-${i}`} cx={cx} icon={item} />);
} else if (item && isObject(item)) {
const icon = (
<Icon key={`icon-${i}`} cx={cx} icon={item['icon']} />
);
if (item['position'] === 'after') {
afterIcon.push(icon);
} else {
beforeIcon.push(icon);
}
} }
return <Icon cx={cx} icon={link.icon} />; }
}); );
afterIcon = link.icon
.filter(item => item.position === 'after')
.map(item => {
if (React.isValidElement(item)) {
return item;
}
return <Icon cx={cx} icon={item.icon} />;
});
} else if (link.icon) {
if (React.isValidElement(link.icon)) {
beforeIcon = link.icon;
} else {
beforeIcon = <Icon cx={cx} icon={link.icon} />;
}
}
const label = const label =
typeof link.label === 'string' typeof link.label === 'string'
@ -642,10 +640,10 @@ export class Navigation extends React.Component<
return { return {
link, link,
label, label,
labelExtra: afterIcon ? ( labelExtra: afterIcon.length ? (
<i className={cx('Nav-Menu-item-icon-after')}>{afterIcon}</i> <i className={cx('Nav-Menu-item-icon-after')}>{afterIcon}</i>
) : null, ) : null,
icon: beforeIcon ? <i>{beforeIcon}</i> : null, icon: beforeIcon.length ? <i>{beforeIcon}</i> : null,
children: children children: children
? this.normalizeNavigations(children, depth + 1) ? this.normalizeNavigations(children, depth + 1)
: [], : [],
@ -654,7 +652,11 @@ export class Navigation extends React.Component<
extra: itemActions extra: itemActions
? render('inline', itemActions, { ? render('inline', itemActions, {
data: createObject(data, link), data: createObject(data, link),
popOverContainer: () => document.body, popOverContainer: popOverContainer
? popOverContainer
: env.getModalContainer
? env.getModalContainer
: () => document.body,
// 点击操作之后 就关闭 因为close方法里执行了preventDefault // 点击操作之后 就关闭 因为close方法里执行了preventDefault
closeOnClick: true closeOnClick: true
}) })
@ -693,7 +695,9 @@ export class Navigation extends React.Component<
popupClassName, popupClassName,
disabled, disabled,
id, id,
render render,
popOverContainer,
env
} = this.props; } = this.props;
const {dropIndicator} = this.state; const {dropIndicator} = this.state;
@ -796,6 +800,13 @@ export class Navigation extends React.Component<
data={data} data={data}
disabled={disabled} disabled={disabled}
onDragStart={this.handleDragStart} onDragStart={this.handleDragStart}
popOverContainer={
popOverContainer
? popOverContainer
: env.getModalContainer
? env.getModalContainer
: () => document.body
}
></Menu> ></Menu>
) : null} ) : null}
<Spinner show={!!loading} overlay loadingConfig={loadingConfig} /> <Spinner show={!!loading} overlay loadingConfig={loadingConfig} />

View File

@ -272,7 +272,7 @@ export class TableBody extends React.Component<TableBodyProps> {
return ( return (
<Com <Com
key={index} key={index}
colSpan={item.colSpan} colSpan={item.colSpan == 1 ? undefined : item.colSpan}
className={item.cellClassName} className={item.cellClassName}
> >
{render(`summary-row/${index}`, item, { {render(`summary-row/${index}`, item, {