mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:58:07 +08:00
Merge branch 'master' into fix-service
This commit is contained in:
commit
a0a1f971f4
@ -98,7 +98,13 @@ import Overlay from './components/Overlay';
|
||||
import PopOver from './components/PopOver';
|
||||
import {FormRenderer} 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 {valueMap as styleMap} from './utils/style-helper';
|
||||
import {RENDERER_TRANSMISSION_OMIT_PROPS} from './SchemaRenderer';
|
||||
@ -195,7 +201,9 @@ export {
|
||||
OnEventProps,
|
||||
FormSchemaBase,
|
||||
filterTarget,
|
||||
CustomStyle
|
||||
CustomStyle,
|
||||
enableDebug,
|
||||
disableDebug
|
||||
};
|
||||
|
||||
export function render(
|
||||
@ -257,13 +265,6 @@ function AMISRenderer({
|
||||
translate
|
||||
} as any;
|
||||
|
||||
if (options.enableAMISDebug) {
|
||||
// 因为里面还有 render
|
||||
setTimeout(() => {
|
||||
enableDebug();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
store = RendererStore.create({}, options);
|
||||
stores[options.session || 'global'] = store;
|
||||
} else {
|
||||
@ -291,6 +292,11 @@ function AMISRenderer({
|
||||
}
|
||||
env.theme = getTheme(theme);
|
||||
|
||||
React.useEffect(() => {
|
||||
env.enableAMISDebug ? enableDebug() : disableDebug();
|
||||
return () => env.enableAMISDebug || disableDebug();
|
||||
}, [env.enableAMISDebug]);
|
||||
|
||||
if (props.locale !== undefined) {
|
||||
env.translate = translate;
|
||||
env.locale = locale;
|
||||
|
@ -223,7 +223,10 @@ export interface ApiObject extends BaseApiObject {
|
||||
api: ApiObject,
|
||||
context: any
|
||||
) => any;
|
||||
requestAdaptor?: (api: ApiObject, context: any) => ApiObject;
|
||||
requestAdaptor?: (
|
||||
api: ApiObject,
|
||||
context: any
|
||||
) => ApiObject | Promise<ApiObject>;
|
||||
/** 是否过滤为空字符串的 query 参数 */
|
||||
filterEmptyQuery?: boolean;
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ export function buildApi(
|
||||
}
|
||||
|
||||
if (api.requestAdaptor && typeof api.requestAdaptor === 'string') {
|
||||
api.requestAdaptor = str2function(
|
||||
api.requestAdaptor = str2AsyncFunction(
|
||||
api.requestAdaptor,
|
||||
'api',
|
||||
'context'
|
||||
@ -84,7 +84,7 @@ export function buildApi(
|
||||
}
|
||||
|
||||
if (api.adaptor && typeof api.adaptor === 'string') {
|
||||
api.adaptor = str2function(
|
||||
api.adaptor = str2AsyncFunction(
|
||||
api.adaptor,
|
||||
'payload',
|
||||
'response',
|
||||
@ -464,12 +464,16 @@ export function wrapFetcher(
|
||||
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;
|
||||
|
||||
if (api.requestAdaptor) {
|
||||
debug('api', 'before requestAdaptor', api);
|
||||
api = api.requestAdaptor(api, data) || api;
|
||||
api = (await api.requestAdaptor(api, data)) || api;
|
||||
debug('api', 'after requestAdaptor', api);
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,10 @@
|
||||
* amis 运行时调试功能,为了避免循环引用,这个组件不要依赖 amis 里的组件
|
||||
*/
|
||||
|
||||
import React, {Component, useEffect, useRef, useState} from 'react';
|
||||
import React, {Component, useEffect, useRef, useState, version} from 'react';
|
||||
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 {observer} from 'mobx-react';
|
||||
import {uuidv4} from './helper';
|
||||
@ -84,16 +85,18 @@ const LogView = observer(({store}: {store: AMISDebugStore}) => {
|
||||
[{log.cat}] {log.msg}
|
||||
</div>
|
||||
{log.ext ? (
|
||||
<JsonView
|
||||
name={null}
|
||||
theme="monokai"
|
||||
src={JSON.parse(log.ext)}
|
||||
collapsed={true}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
collapseStringsAfterLength={ellipsisThreshold}
|
||||
iconStyle="square"
|
||||
/>
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<JsonView
|
||||
name={null}
|
||||
theme="monokai"
|
||||
src={JSON.parse(log.ext)}
|
||||
collapsed={true}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
collapseStringsAfterLength={ellipsisThreshold}
|
||||
iconStyle="square"
|
||||
/>
|
||||
</React.Suspense>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
@ -126,16 +129,18 @@ const AMISDebug = observer(({store}: {store: AMISDebugStore}) => {
|
||||
stackDataView.push(
|
||||
<div key={`data-${level}`}>
|
||||
<h3>Data Level-{level}</h3>
|
||||
<JsonView
|
||||
key={`dataview-${stack}`}
|
||||
name={null}
|
||||
theme="monokai"
|
||||
src={stack}
|
||||
collapsed={level === 0 ? false : true}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
iconStyle="square"
|
||||
/>
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<JsonView
|
||||
key={`dataview-${stack}`}
|
||||
name={null}
|
||||
theme="monokai"
|
||||
src={stack}
|
||||
collapsed={level === 0 ? false : true}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
iconStyle="square"
|
||||
/>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
);
|
||||
level += 1;
|
||||
@ -319,7 +324,7 @@ function handleMouseclick(e: MouseEvent) {
|
||||
}
|
||||
const dom = e.target as HTMLElement;
|
||||
const target = dom.closest(`[data-debug-id]`);
|
||||
if (target) {
|
||||
if (target && !target.closest('.AMISDebug')) {
|
||||
store.activeId = target.getAttribute('data-debug-id')!;
|
||||
store.tab = 'inspect';
|
||||
}
|
||||
@ -366,6 +371,7 @@ autorun(() => {
|
||||
|
||||
// 页面中只能有一个实例
|
||||
let isEnabled = false;
|
||||
let unmount: () => void;
|
||||
|
||||
export function enableDebug() {
|
||||
if (isEnabled) {
|
||||
@ -376,7 +382,21 @@ export function enableDebug() {
|
||||
const amisDebugElement = document.createElement('div');
|
||||
document.body.appendChild(amisDebugElement);
|
||||
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(amisActiveBox);
|
||||
@ -384,6 +404,18 @@ export function enableDebug() {
|
||||
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 {
|
||||
renderer: any;
|
||||
children?: React.ReactNode;
|
||||
|
@ -40,6 +40,7 @@
|
||||
|
||||
&-tab {
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #3d3d3d;
|
||||
}
|
||||
|
||||
&-tab > button {
|
||||
@ -90,6 +91,7 @@
|
||||
&-content {
|
||||
pointer-events: all;
|
||||
display: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-resize {
|
||||
@ -122,7 +124,7 @@
|
||||
|
||||
&.is-expanded {
|
||||
width: 420px;
|
||||
overflow: auto;
|
||||
|
||||
background: #272821;
|
||||
color: #cccccc;
|
||||
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
|
||||
@ -168,6 +170,37 @@
|
||||
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 {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
@ -89,11 +89,20 @@
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: var(--drawer-content-paddingTop) var(--drawer-content-paddingRight)
|
||||
padding: 0 var(--drawer-content-paddingRight)
|
||||
var(--drawer-content-paddingBottom) var(--drawer-content-paddingLeft);
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
|
||||
// 因为如果成员里面有 position:sticky 的内容
|
||||
// 用 padding 会导致位置不正确
|
||||
// 所以改成这种写法
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
height: var(--drawer-content-paddingTop);
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
|
@ -350,6 +350,7 @@
|
||||
|
||||
&-submenu-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.#{$ns}Nav-Menu-item-wrap {
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @author fex
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {version} from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import Modal from './Modal';
|
||||
import Button from './Button';
|
||||
@ -11,6 +11,7 @@ import {ClassNamesFn, themeable, ThemeProps} from 'amis-core';
|
||||
import {LocaleProps, localeable} from 'amis-core';
|
||||
import Html from './Html';
|
||||
import type {PlainObject} from 'amis-core';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
export interface AlertProps extends ThemeProps, LocaleProps {
|
||||
container?: any;
|
||||
confirmText?: string;
|
||||
@ -52,13 +53,21 @@ export interface AlertState {
|
||||
|
||||
export class Alert extends React.Component<AlertProps, AlertState> {
|
||||
static instance: any = null;
|
||||
static getInstance() {
|
||||
static async getInstance() {
|
||||
if (!Alert.instance) {
|
||||
console.warn('Alert 组件应该没有被渲染,所以隐性的渲染到 body 了');
|
||||
const container = document.body;
|
||||
const div = document.createElement('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;
|
||||
@ -346,23 +355,35 @@ function renderForm(
|
||||
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,
|
||||
title
|
||||
) => Alert.getInstance().alert(content, title);
|
||||
) => {
|
||||
const instance = await Alert.getInstance();
|
||||
return instance.alert(content, title);
|
||||
};
|
||||
export const confirm: (
|
||||
content: string | React.ReactNode,
|
||||
title?: string,
|
||||
optionsOrCofnrimText?: string | ConfirmOptions,
|
||||
cancelText?: string
|
||||
) => Promise<any> = (content, title, optionsOrCofnrimText, cancelText) =>
|
||||
Alert.getInstance().confirm(content, title, optionsOrCofnrimText, cancelText);
|
||||
) => Promise<any> = async (
|
||||
content,
|
||||
title,
|
||||
optionsOrCofnrimText,
|
||||
cancelText
|
||||
) => {
|
||||
const instance = await Alert.getInstance();
|
||||
return instance.confirm(content, title, optionsOrCofnrimText, cancelText);
|
||||
};
|
||||
export const prompt: (
|
||||
controls: any,
|
||||
defaultvalue?: any,
|
||||
title?: string,
|
||||
confirmText?: string
|
||||
) => Promise<any> = (controls, defaultvalue, title, confirmText) =>
|
||||
Alert.getInstance().prompt(controls, defaultvalue, title, confirmText);
|
||||
) => Promise<any> = async (controls, defaultvalue, title, confirmText) => {
|
||||
const instance = await Alert.getInstance();
|
||||
return instance.prompt(controls, defaultvalue, title, confirmText);
|
||||
};
|
||||
export const FinnalAlert = themeable(localeable(Alert));
|
||||
export default FinnalAlert;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {ClassNamesFn, themeable} from 'amis-core';
|
||||
import React from 'react';
|
||||
import React, {version} from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {autobind, calculatePosition} from 'amis-core';
|
||||
import Transition, {
|
||||
@ -7,6 +7,7 @@ import Transition, {
|
||||
ENTERING,
|
||||
EXITING
|
||||
} from 'react-transition-group/Transition';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
const fadeStyles: {
|
||||
[propName: string]: string;
|
||||
} = {
|
||||
@ -49,12 +50,20 @@ export class ContextMenu extends React.Component<
|
||||
ContextMenuState
|
||||
> {
|
||||
static instance: any = null;
|
||||
static getInstance() {
|
||||
static async getInstance() {
|
||||
if (!ContextMenu.instance) {
|
||||
const container = document.body;
|
||||
const div = document.createElement('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;
|
||||
@ -309,5 +318,7 @@ export function openContextMenus(
|
||||
menus: Array<MenuItem | MenuDivider>,
|
||||
onClose?: () => void
|
||||
) {
|
||||
return ContextMenu.getInstance().openContextMenus(info, menus, onClose);
|
||||
return ContextMenu.getInstance().then(instance =>
|
||||
instance.openContextMenus(info, menus, onClose)
|
||||
);
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ exports[`doAction:service reload 1`] = `
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="Amis Renderer"
|
||||
value="amis"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -321,7 +321,7 @@ exports[`doAction:service reload 2`] = `
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="Amis Renderer"
|
||||
value="amis"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -342,7 +342,7 @@ test('Renderers:Action tooltip', async () => {
|
||||
// });
|
||||
|
||||
// 14. confirmText
|
||||
test('Renderers:Action with confirmText & actionType ajax', () => {
|
||||
test('Renderers:Action with confirmText & actionType ajax', async () => {
|
||||
const fetcher = jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
@ -372,7 +372,7 @@ test('Renderers:Action with confirmText & actionType ajax', () => {
|
||||
)
|
||||
);
|
||||
fireEvent.click(container.querySelector('.cxd-Button'));
|
||||
wait(500);
|
||||
await wait(500);
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
|
||||
expect(baseElement.querySelector('.cxd-Modal-content')!).toHaveTextContent(
|
||||
@ -380,14 +380,16 @@ test('Renderers:Action with confirmText & actionType ajax', () => {
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('取消'));
|
||||
wait(500);
|
||||
await wait(500);
|
||||
expect(fetcher).not.toBeCalled();
|
||||
|
||||
// fireEvent.click(container.querySelector('.cxd-Button'));
|
||||
// wait(500);
|
||||
// fireEvent.click(getByText('确认'));
|
||||
// fetcher 不生效
|
||||
// expect(fetcher).toBeCalled();
|
||||
fireEvent.click(container.querySelector('.cxd-Button'));
|
||||
await wait(500);
|
||||
fireEvent.click(getByText('确认'));
|
||||
|
||||
await wait(200);
|
||||
// fetcher 该被执行了
|
||||
expect(fetcher).toBeCalled();
|
||||
});
|
||||
|
||||
// 15.Action 作为容器组件
|
||||
|
@ -136,13 +136,15 @@ test('Renderer: input-table with default value column', async () => {
|
||||
await wait(200);
|
||||
|
||||
expect(onSubmitCallbackFn).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmitCallbackFn.mock.calls[0][0]).toEqual({
|
||||
table: [
|
||||
{a: 'a1', b: 'b1', c: 'a1'},
|
||||
{a: 'a2', b: 'b2', c: 'a2'},
|
||||
{a: 'a3', b: 'b3', c: 'a3'}
|
||||
]
|
||||
});
|
||||
expect(onSubmitCallbackFn.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
table: [
|
||||
{a: 'a1', b: 'b1', c: 'a1'},
|
||||
{a: 'a2', b: 'b2', c: 'a2'},
|
||||
{a: 'a3', b: 'b3', c: 'a3'}
|
||||
]
|
||||
})
|
||||
);
|
||||
}, 10000);
|
||||
|
||||
test('Renderer:input table add', async () => {
|
||||
|
@ -517,3 +517,181 @@ test('Renderer:Nav with itemActions', async () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
@ -208,6 +208,228 @@ exports[`Renderer:Nav 1`] = `
|
||||
</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`] = `
|
||||
<div>
|
||||
<div
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {render as amisRender} from '../../src';
|
||||
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';
|
||||
|
||||
test('api:buildApi', () => {
|
||||
@ -358,3 +358,74 @@ test('api:isvalidapi', () => {
|
||||
)
|
||||
).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'
|
||||
});
|
||||
});
|
||||
|
@ -465,6 +465,10 @@ export default class Dialog extends React.Component<DialogProps> {
|
||||
syncLocation: false // 弹框中的 crud 一般不需要同步地址栏
|
||||
};
|
||||
|
||||
if (this.props.size === 'full') {
|
||||
subProps.affixOffsetTop = 0;
|
||||
}
|
||||
|
||||
if (!(body as Schema).type) {
|
||||
return render(`body${key ? `/${key}` : ''}`, body, subProps);
|
||||
}
|
||||
|
@ -465,7 +465,8 @@ export default class Drawer extends React.Component<DrawerProps> {
|
||||
onInit: this.handleFormInit,
|
||||
onSaved: this.handleFormSaved,
|
||||
onActionSensor: this.handleActionSensor,
|
||||
syncLocation: false
|
||||
syncLocation: false,
|
||||
affixOffsetTop: 0
|
||||
};
|
||||
|
||||
if (schema.type === 'form') {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import isString from 'lodash/isString';
|
||||
import {
|
||||
Renderer,
|
||||
RendererEnv,
|
||||
@ -27,7 +28,7 @@ import {
|
||||
} from 'amis-core';
|
||||
import {isEffectiveApi} 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 {RemoteOptionsProps, withRemoteConfig} from 'amis-ui';
|
||||
import {Spinner, Menu} from 'amis-ui';
|
||||
@ -143,11 +144,6 @@ export interface NavOverflow {
|
||||
* 自定义样式
|
||||
*/
|
||||
style?: React.CSSProperties;
|
||||
|
||||
/**
|
||||
* 菜单DOM挂载点
|
||||
*/
|
||||
popOverContainer?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -319,6 +315,10 @@ export interface NavigationProps
|
||||
data: Object;
|
||||
reload?: any;
|
||||
overflow?: NavOverflow;
|
||||
/**
|
||||
* 菜单DOM挂载点
|
||||
*/
|
||||
popOverContainer?: () => HTMLElement;
|
||||
}
|
||||
|
||||
export interface IDropInfo {
|
||||
@ -550,6 +550,8 @@ export class Navigation extends React.Component<
|
||||
mode,
|
||||
itemActions,
|
||||
render,
|
||||
popOverContainer,
|
||||
env,
|
||||
classnames: cx,
|
||||
data
|
||||
} = this.props;
|
||||
@ -562,32 +564,28 @@ export class Navigation extends React.Component<
|
||||
}
|
||||
|
||||
return links.map((link: Link) => {
|
||||
let beforeIcon = null;
|
||||
let afterIcon = null;
|
||||
if (Array.isArray(link.icon)) {
|
||||
beforeIcon = link.icon
|
||||
.filter(item => item.position === 'before')
|
||||
.map(item => {
|
||||
const beforeIcon: Array<any> = [];
|
||||
const afterIcon: Array<any> = [];
|
||||
|
||||
link.icon &&
|
||||
(Array.isArray(link.icon) ? link.icon : [link.icon]).forEach(
|
||||
(item, i) => {
|
||||
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 =
|
||||
typeof link.label === 'string'
|
||||
@ -642,10 +640,10 @@ export class Navigation extends React.Component<
|
||||
return {
|
||||
link,
|
||||
label,
|
||||
labelExtra: afterIcon ? (
|
||||
labelExtra: afterIcon.length ? (
|
||||
<i className={cx('Nav-Menu-item-icon-after')}>{afterIcon}</i>
|
||||
) : null,
|
||||
icon: beforeIcon ? <i>{beforeIcon}</i> : null,
|
||||
icon: beforeIcon.length ? <i>{beforeIcon}</i> : null,
|
||||
children: children
|
||||
? this.normalizeNavigations(children, depth + 1)
|
||||
: [],
|
||||
@ -654,7 +652,11 @@ export class Navigation extends React.Component<
|
||||
extra: itemActions
|
||||
? render('inline', itemActions, {
|
||||
data: createObject(data, link),
|
||||
popOverContainer: () => document.body,
|
||||
popOverContainer: popOverContainer
|
||||
? popOverContainer
|
||||
: env.getModalContainer
|
||||
? env.getModalContainer
|
||||
: () => document.body,
|
||||
// 点击操作之后 就关闭 因为close方法里执行了preventDefault
|
||||
closeOnClick: true
|
||||
})
|
||||
@ -693,7 +695,9 @@ export class Navigation extends React.Component<
|
||||
popupClassName,
|
||||
disabled,
|
||||
id,
|
||||
render
|
||||
render,
|
||||
popOverContainer,
|
||||
env
|
||||
} = this.props;
|
||||
const {dropIndicator} = this.state;
|
||||
|
||||
@ -796,6 +800,13 @@ export class Navigation extends React.Component<
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
onDragStart={this.handleDragStart}
|
||||
popOverContainer={
|
||||
popOverContainer
|
||||
? popOverContainer
|
||||
: env.getModalContainer
|
||||
? env.getModalContainer
|
||||
: () => document.body
|
||||
}
|
||||
></Menu>
|
||||
) : null}
|
||||
<Spinner show={!!loading} overlay loadingConfig={loadingConfig} />
|
||||
|
@ -272,7 +272,7 @@ export class TableBody extends React.Component<TableBodyProps> {
|
||||
return (
|
||||
<Com
|
||||
key={index}
|
||||
colSpan={item.colSpan}
|
||||
colSpan={item.colSpan == 1 ? undefined : item.colSpan}
|
||||
className={item.cellClassName}
|
||||
>
|
||||
{render(`summary-row/${index}`, item, {
|
||||
|
Loading…
Reference in New Issue
Block a user