chore: reload 动作改成等待完成(原来不等待只发送指令), 同时支持获取 service 结果

This commit is contained in:
2betop 2024-10-17 11:20:36 +08:00 committed by lmaomaoz
parent fcf6d763ab
commit c6d2f8289c
13 changed files with 374 additions and 280 deletions

View File

@ -2112,6 +2112,73 @@ run action ajax
}
```
#### 获得刷新后的结果
> 6.9.0 及以上版本
通过配置 outputVar 可以拿到刷新后的结果,例如以下示例,点击按钮后,会串行按顺序刷新 service 1 和 service2 同时 service2 可以拿到 service1 的返回值。
```schema
{
"type": "page",
"body": [
{
"type": "button",
"label": "刷新Service数据",
"level": "primary",
"className": "mb-2",
"onEvent": {
"click": {
"actions": [
{
"componentId": "service_reload",
"actionType": "reload",
"outputVar": "service1"
},
{
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "service 的数据返回为: ${service1|json}"
}
},
{
"componentId": "service_reload2",
"actionType": "reload",
"data": {
"date": "${service1.date}"
}
}
]
}
}
},
{
"type": "service",
"api": {
"method": "get",
"url": "/api/mock2/form/initData",
"trackExpression": "none"
},
"body": [],
"initFetch": false,
"id": "service_reload"
},
{
"type": "service",
"api": {
"method": "get",
"url": "/api/mock2/form/initData?date=${date}",
"trackExpression": "none"
},
"body": [],
"initFetch": false,
"id": "service_reload2"
}
]
}
```
**动作属性**
| 属性名 | 类型 | 默认值 | 说明 |

View File

@ -115,7 +115,7 @@ export interface IScopedContext {
ignoreScope?: IScopedContext
) => ScopedComponentType | undefined;
getComponents: () => Array<ScopedComponentType>;
reload: (target: string, ctx: RendererData) => void;
reload: (target: string, ctx: RendererData) => void | Promise<void>;
send: (target: string, ctx: RendererData) => void;
close: (target: string) => void;
closeById: (target: string) => void;

View File

@ -1,5 +1,5 @@
import {RendererEvent} from '../utils/renderer-event';
import {createObject} from '../utils/helper';
import {createObject, extendObject} from '../utils/helper';
import {
RendererAction,
ListenerAction,
@ -82,7 +82,7 @@ export class CmptAction implements RendererAction {
// 刷新
if (action.actionType === 'reload') {
return component?.reload?.(
const result = await component?.reload?.(
undefined,
action.data,
event.data,
@ -90,6 +90,16 @@ export class CmptAction implements RendererAction {
dataMergeMode === 'override',
action.args
);
if (result && action.outputVar) {
event.setData(
extendObject(event.data, {
[action.outputVar]: result
})
);
}
return result;
}
// 校验表单项

View File

@ -894,6 +894,7 @@ export default class Form extends React.Component<FormProps, object> {
// 派发初始化接口请求完成事件
this.dispatchInited(result);
return store.data;
}
receive(values: object, name?: string, replace?: boolean) {
@ -904,7 +905,7 @@ export default class Form extends React.Component<FormProps, object> {
}
silentReload(target?: string, query?: any) {
this.reload(target, query, undefined, true);
return this.reload(target, query, undefined, true);
}
initInterval(value: any) {
@ -1127,7 +1128,7 @@ export default class Form extends React.Component<FormProps, object> {
onChange && onChange.apply(null, changeProps);
}
store.clearRestError();
isAlive(store) && store.clearRestError();
// 只有主动修改表单项触发的 change 才会触发 submit
if (!emitedFromWatch && (submit || (submitOnChange && store.inited))) {
@ -1246,6 +1247,10 @@ export default class Form extends React.Component<FormProps, object> {
await this.flush();
}
if (!isAlive(store)) {
return;
}
if (trimValues) {
store.trimValues();
}

View File

@ -601,6 +601,19 @@ export function wrapFetcher(
// 如果发送适配器中设置了 mockResponse
// 则直接跳过请求发送
if (api.mockResponse) {
console.debug(
`fetch api ${api.url}${
api.data
? `?${
typeof api.data === 'string'
? api.data
: qsstringify(api.data, api.qsOptions)
}`
: ''
} with mock response`,
api.mockResponse,
api
);
return wrapAdaptor(Promise.resolve(api.mockResponse) as any, api, data);
}

View File

@ -1264,6 +1264,17 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
value: 'override'
}
]
},
{
name: 'outputVar',
type: 'input-text',
label: '刷新后数据结果',
placeholder: '请输入变量名称',
description:
'如果想要拿到目标组件刷新后的数据,请在此处输入变量名称',
mode: 'horizontal',
size: 'lg',
value: ''
}
]
},

View File

@ -240,7 +240,7 @@ export class App extends React.Component<AppProps, object> {
ctx?: any,
silent?: boolean,
replace?: boolean
) {
): Promise<any> {
if (query) {
return this.receive(query, undefined, replace);
}
@ -277,13 +277,15 @@ export class App extends React.Component<AppProps, object> {
);
}
}
return store.data;
}
receive(values: object, subPath?: string, replace?: boolean) {
async receive(values: object, subPath?: string, replace?: boolean) {
const {store} = this.props;
store.updateData(values, undefined, replace);
this.reload();
return this.reload();
}
/**

View File

@ -1241,7 +1241,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
});
}
search(
async search(
values?: any,
silent?: boolean,
clearSelection?: boolean,
@ -1300,98 +1300,87 @@ export default class CRUD extends React.Component<CRUDProps, any> {
'options'
) as any)
: undefined;
isEffectiveApi(api, data)
? store
.fetchInitData(api, data, {
successMessage: messages && messages.fetchSuccess,
errorMessage: messages && messages.fetchFailed,
autoAppend: true,
forceReload,
loadDataOnce,
source,
silent,
pageField,
perPageField,
loadDataMode,
syncResponse2Query,
columns: store.columns ?? columns,
matchFunc
if (isEffectiveApi(api, data)) {
const value = await store.fetchInitData(api, data, {
successMessage: messages && messages.fetchSuccess,
errorMessage: messages && messages.fetchFailed,
autoAppend: true,
forceReload,
loadDataOnce,
source,
silent,
pageField,
perPageField,
loadDataMode,
syncResponse2Query,
columns: store.columns ?? columns,
matchFunc
});
if (!isAlive(store)) {
return value;
}
const {page, lastPage, msg, error} = store;
if (isInit) {
// 初始化请求完成
const rendererEvent = await dispatchEvent?.(
'fetchInited',
createObject(this.props.data, {
responseData: value?.ok ? store.data ?? {} : value,
responseStatus:
value?.status === undefined ? (error ? 1 : 0) : value?.status,
responseMsg: msg
})
.then(async value => {
if (!isAlive(store)) {
return value;
}
);
const {page, lastPage, data, msg, error} = store;
if (rendererEvent?.prevented) {
return store.data;
}
}
if (isInit) {
// 初始化请求完成
const rendererEvent = await dispatchEvent?.(
'fetchInited',
createObject(this.props.data, {
responseData: value?.ok ? data ?? {} : value,
responseStatus:
value?.status === undefined
? error
? 1
: 0
: value?.status,
responseMsg: msg
})
);
// 空列表 且 页数已经非法超出,则跳转到最后的合法页数
if (
!store.data.items.length &&
!interval &&
page > 1 &&
lastPage < page
) {
this.search(
{
...store.query,
[pageField || 'page']: lastPage
},
false,
undefined
);
}
if (rendererEvent?.prevented) {
return;
}
}
value?.ok && // 接口正常返回才继续轮训
interval &&
this.mounted &&
(!stopAutoRefreshWhen ||
!(
(stopAutoRefreshWhenModalIsOpen && store.hasModalOpened) ||
evalExpression(
stopAutoRefreshWhen,
createObject(store.data, store.query)
)
)) &&
(this.timer = setTimeout(
silentPolling
? this.silentSearch.bind(this, undefined, undefined, true)
: this.search.bind(this, undefined, undefined, undefined, true),
Math.max(interval, 1000)
));
} else if (source) {
store.initFromScope(data, source, {
columns: store.columns ?? columns,
matchFunc
});
}
// 空列表 且 页数已经非法超出,则跳转到最后的合法页数
if (
!store.data.items.length &&
!interval &&
page > 1 &&
lastPage < page
) {
this.search(
{
...store.query,
[pageField || 'page']: lastPage
},
false,
undefined
);
}
value?.ok && // 接口正常返回才继续轮训
interval &&
this.mounted &&
(!stopAutoRefreshWhen ||
!(
(stopAutoRefreshWhenModalIsOpen && store.hasModalOpened) ||
evalExpression(
stopAutoRefreshWhen,
createObject(store.data, store.query)
)
)) &&
(this.timer = setTimeout(
silentPolling
? this.silentSearch.bind(this, undefined, undefined, true)
: this.search.bind(
this,
undefined,
undefined,
undefined,
true
),
Math.max(interval, 1000)
));
return value;
})
: source &&
store.initFromScope(data, source, {
columns: store.columns ?? columns,
matchFunc
});
return store.data;
}
silentSearch(values?: object, clearSelection?: boolean, forceReload = false) {
@ -1862,7 +1851,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
perPageField,
replace
);
this.search(
return this.search(
undefined,
undefined,
clearSelection ?? replace,
@ -1880,7 +1869,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
if (query) {
return this.receive(query, undefined, replace, resetPage, true);
} else {
this.search(undefined, undefined, true, true);
return this.search(undefined, undefined, true, true);
}
}
@ -1891,7 +1880,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
resetPage?: boolean,
clearSelection?: boolean
) {
this.handleQuery(values, true, replace, resetPage, clearSelection);
return this.handleQuery(values, true, replace, resetPage, clearSelection);
}
reloadTarget(target: string, data: any) {
@ -2834,7 +2823,7 @@ export class CRUDRenderer extends CRUD {
return super.reload(subpath, query, replace, args?.resetPage ?? true);
}
receive(
async receive(
values: any,
subPath?: string,
replace?: boolean,

View File

@ -612,7 +612,7 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
/**
*
*/
getData(
async getData(
/** 静默更新,不显示加载状态 */
silent?: boolean,
/** 清空已选择数据 */
@ -667,49 +667,49 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
store.changePerPage(perPage);
}
isEffectiveApi(api, data)
? store
.fetchInitData(api, data, {
successMessage: messages && messages.fetchSuccess,
errorMessage: messages && messages.fetchFailed,
autoAppend: true,
forceReload,
loadDataOnce,
source,
silent,
pageField,
perPageField,
loadDataMode,
syncResponse2Query,
columns: store.columns ?? columns,
isTable2: true
})
.then(value => {
value?.ok && // 接口正常返回才继续轮训
interval &&
!this.stopingAutoRefresh &&
this.mounted &&
(!stopAutoRefreshWhen ||
!(
stopAutoRefreshWhen &&
evalExpression(
stopAutoRefreshWhen,
createObject(store.data, store.query)
)
)) &&
// 弹窗期间不进行刷新
(!stopAutoRefreshWhenModalIsOpen ||
(!store.dialogOpen && !store?.parentStore?.dialogOpen)) &&
(this.timer = setTimeout(
this.getData.bind(this, silentPolling, undefined, true),
Math.max(interval, 1000)
));
return value;
})
: source &&
store.initFromScope(data, source, {
columns: store.columns ?? columns
});
if (isEffectiveApi(api, data)) {
const value = await store.fetchInitData(api, data, {
successMessage: messages && messages.fetchSuccess,
errorMessage: messages && messages.fetchFailed,
autoAppend: true,
forceReload,
loadDataOnce,
source,
silent,
pageField,
perPageField,
loadDataMode,
syncResponse2Query,
columns: store.columns ?? columns,
isTable2: true
});
value?.ok && // 接口正常返回才继续轮训
interval &&
!this.stopingAutoRefresh &&
this.mounted &&
(!stopAutoRefreshWhen ||
!(
stopAutoRefreshWhen &&
evalExpression(
stopAutoRefreshWhen,
createObject(store.data, store.query)
)
)) &&
// 弹窗期间不进行刷新
(!stopAutoRefreshWhenModalIsOpen ||
(!store.dialogOpen && !store?.parentStore?.dialogOpen)) &&
(this.timer = setTimeout(
this.getData.bind(this, silentPolling, undefined, true),
Math.max(interval, 1000)
));
} else if (source) {
store.initFromScope(data, source, {
columns: store.columns ?? columns
});
}
return store.data;
}
@autobind
@ -1055,19 +1055,19 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
pageField,
perPageField
);
this.getData(undefined, undefined, forceReload);
return this.getData(undefined, undefined, forceReload);
}
reload(subpath?: string, query?: any) {
if (query) {
return this.receive(query);
} else {
this.getData(undefined, undefined, true);
return this.getData(undefined, undefined, true);
}
}
receive(values: object) {
this.handleQuerySearch(values, true);
return this.handleQuerySearch(values, true);
}
@autobind
@ -1630,7 +1630,7 @@ export class CRUD2Renderer extends CRUD2 {
scoped.unRegisterComponent(this);
}
reload(subpath?: string, query?: any, ctx?: any) {
async reload(subpath?: string, query?: any, ctx?: any) {
const scoped = this.context as IScopedContext;
if (subpath) {
return scoped.reload(
@ -1642,7 +1642,7 @@ export class CRUD2Renderer extends CRUD2 {
return super.reload(subpath, query);
}
receive(values: any, subPath?: string) {
async receive(values: any, subPath?: string) {
const scoped = this.context as IScopedContext;
if (subPath) {
return scoped.send(subPath, values);

View File

@ -468,7 +468,7 @@ export class Chart extends React.Component<ChartProps> {
});
}
reload(
async reload(
subpath?: string,
query?: any,
ctx?: any,
@ -492,60 +492,61 @@ export class Chart extends React.Component<ChartProps> {
this.echarts?.showLoading();
store.markFetching(true);
env
.fetcher(api, store.data, {
try {
const result = await env.fetcher(api, store.data, {
cancelExecutor: (executor: Function) => (this.reloadCancel = executor)
})
.then(result => {
isAlive(store) && store.markFetching(false);
if (!result.ok) {
!(api as ApiObject)?.silent &&
env.notify(
'error',
(api as ApiObject)?.messages?.failed ??
(result.msg || __('fetchFailed')),
result.msgTimeout !== undefined
? {
closeButton: true,
timeout: result.msgTimeout
}
: undefined
);
return;
}
delete this.reloadCancel;
const data = normalizeApiResponseData(result.data);
// 说明返回的是数据接口。
if (!data.series && this.props.config) {
const ctx = createObject(this.props.data, data);
this.renderChart(this.props.config, ctx);
} else {
this.renderChart(result.data || {});
}
this.echarts?.hideLoading();
let curInterval = this.props.interval;
if (curInterval && isString(curInterval)) {
curInterval = Number.parseInt(curInterval);
}
curInterval &&
this.mounted &&
(this.timer = setTimeout(this.reload, Math.max(curInterval, 1000)));
})
.catch(reason => {
if (env.isCancel(reason)) {
return;
}
isAlive(store) && store.markFetching(false);
!(api as ApiObject)?.silent && env.notify('error', reason);
this.echarts?.hideLoading();
});
isAlive(store) && store.markFetching(false);
if (!result.ok) {
!(api as ApiObject)?.silent &&
env.notify(
'error',
(api as ApiObject)?.messages?.failed ??
(result.msg || __('fetchFailed')),
result.msgTimeout !== undefined
? {
closeButton: true,
timeout: result.msgTimeout
}
: undefined
);
return;
}
delete this.reloadCancel;
const data = normalizeApiResponseData(result.data);
// 说明返回的是数据接口。
if (!data.series && this.props.config) {
const ctx = createObject(this.props.data, data);
this.renderChart(this.props.config, ctx);
} else {
this.renderChart(result.data || {});
}
this.echarts?.hideLoading();
let curInterval = this.props.interval;
if (curInterval && isString(curInterval)) {
curInterval = Number.parseInt(curInterval);
}
curInterval &&
this.mounted &&
(this.timer = setTimeout(this.reload, Math.max(curInterval, 1000)));
return store.data;
} catch (reason) {
if (env.isCancel(reason)) {
return;
}
isAlive(store) && store.markFetching(false);
!(api as ApiObject)?.silent && env.notify('error', reason);
this.echarts?.hideLoading();
}
}
receive(data: object, subPath?: string, replace?: boolean) {

View File

@ -701,7 +701,7 @@ export default class Page extends React.Component<PageProps> {
});
}
reload(
async reload(
subpath?: any,
query?: any,
ctx?: any,
@ -715,12 +715,14 @@ export default class Page extends React.Component<PageProps> {
const {store, initApi} = this.props;
clearTimeout(this.timer);
isEffectiveApi(initApi, store.data) &&
store
.fetchData(initApi, store.data, {
silent
})
.then(this.initInterval);
if (isEffectiveApi(initApi, store.data)) {
const value = await store.fetchData(initApi, store.data, {
silent
});
this.initInterval(value);
}
return store.data;
}
receive(values: object, subPath?: string, replace?: boolean) {

View File

@ -586,13 +586,13 @@ export default class Service extends React.Component<ServiceProps> {
return value;
}
reload(
async reload(
subpath?: string,
query?: any,
ctx?: RendererData,
silent?: boolean,
replace?: boolean
) {
): Promise<any> {
if (query) {
return this.receive(query, undefined, replace);
}
@ -611,44 +611,40 @@ export default class Service extends React.Component<ServiceProps> {
clearTimeout(this.timer);
if (isEffectiveApi(schemaApi, store.data)) {
store
.fetchSchema(schemaApi, store.data, {
successMessage: fetchSuccess,
errorMessage: fetchFailed
})
.then(res => {
this.runDataProvider('onApiFetched');
this.afterSchemaFetch(res);
});
const res = await store.fetchSchema(schemaApi, store.data, {
successMessage: fetchSuccess,
errorMessage: fetchFailed
});
await this.runDataProvider('onApiFetched');
this.afterSchemaFetch(res);
}
if (isEffectiveApi(api, store.data)) {
store
.fetchData(api, store.data, {
silent,
successMessage: fetchSuccess,
errorMessage: fetchFailed
})
.then(res => {
this.runDataProvider('onSchemaApiFetched');
this.afterDataFetch(res);
});
const res = await store.fetchData(api, store.data, {
silent,
successMessage: fetchSuccess,
errorMessage: fetchFailed
});
await this.runDataProvider('onSchemaApiFetched');
this.afterDataFetch(res);
}
if (dataProvider) {
this.runDataProvider('inited');
await this.runDataProvider('inited');
}
return store.data;
}
silentReload(target?: string, query?: any) {
this.reload(target, query, undefined, true);
}
receive(values: object, subPath?: string, replace?: boolean) {
async receive(values: object, subPath?: string, replace?: boolean) {
const {store} = this.props;
store.updateData(values, undefined, replace);
this.reload();
return this.reload();
}
handleQuery(query: any) {
@ -878,7 +874,7 @@ export class ServiceRenderer extends Service {
scoped.registerComponent(this as ScopedComponentType);
}
reload(
async reload(
subpath?: string,
query?: any,
ctx?: any,
@ -896,7 +892,7 @@ export class ServiceRenderer extends Service {
return super.reload(subpath, query, ctx, silent, replace);
}
receive(values: any, subPath?: string, replace?: boolean) {
async receive(values: any, subPath?: string, replace?: boolean) {
const scoped = this.context as IScopedContext;
if (subPath) {
return scoped.send(subPath, values);

View File

@ -449,7 +449,7 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
throw new Error('Please implements this!');
}
reload(
async reload(
subPath?: string,
query?: any,
ctx?: any,
@ -469,60 +469,58 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
} = this.props;
if (isEffectiveApi(initApi, store.data) && this.state.currentStep === 1) {
store
.fetchInitData(initApi, store.data, {
successMessage: fetchSuccess,
errorMessage: fetchFailed,
onSuccess: () => {
if (
!isEffectiveApi(initAsyncApi, store.data) ||
store.data[initFinishedField || 'finished']
) {
return;
}
return until(
() => store.checkRemote(initAsyncApi, store.data),
(ret: any) => ret && ret[initFinishedField || 'finished'],
cancel => (this.asyncCancel = cancel)
);
}
})
.then(value => {
const state = {
currentStep: 1
};
const value = await store.fetchInitData(initApi, store.data, {
successMessage: fetchSuccess,
errorMessage: fetchFailed,
onSuccess: () => {
if (
value &&
value.data &&
(typeof value.data.step === 'number' ||
(typeof value.data.step === 'string' &&
/^\d+$/.test(value.data.step)))
!isEffectiveApi(initAsyncApi, store.data) ||
store.data[initFinishedField || 'finished']
) {
state.currentStep = toNumber(value.data.step, 1);
return;
}
this.setState(state, () => {
// 如果 initApi 返回的状态是正在提交,则进入轮顺状态。
if (
value &&
value.data &&
(value.data.submiting || value.data.submited)
) {
this.checkSubmit();
}
});
return value;
});
return until(
() => store.checkRemote(initAsyncApi, store.data),
(ret: any) => ret && ret[initFinishedField || 'finished'],
cancel => (this.asyncCancel = cancel)
);
}
});
const state = {
currentStep: 1
};
if (
value &&
value.data &&
(typeof value.data.step === 'number' ||
(typeof value.data.step === 'string' &&
/^\d+$/.test(value.data.step)))
) {
state.currentStep = toNumber(value.data.step, 1);
}
this.setState(state, () => {
// 如果 initApi 返回的状态是正在提交,则进入轮顺状态。
if (
value &&
value.data &&
(value.data.submiting || value.data.submited)
) {
this.checkSubmit();
}
});
}
return store.data;
}
receive(values: object, subPath?: string, replace?: boolean) {
receive(values: object, subPath?: string, replace?: boolean): Promise<any> {
const {store} = this.props;
store.updateData(values, undefined, replace);
this.reload();
return this.reload();
}
@autobind