import { assert } from 'oak-domain/lib/utils/assert'; import { CheckerType, EntityDict, OakException, OakInputIllegalException, OakUserException, } from 'oak-domain/lib/types'; import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; import { NamedFilterItem, NamedSorterItem } from './types/NamedCondition'; import { OakComponentOption, ComponentFullThisType, ActionDef, RowWithActions, ComponentProps, OakComponentData, } from './types/Page'; import { cloneDeep, unset } from 'oak-domain/lib/utils/lodash'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { MessageProps } from './types/Message'; import { judgeRelation } from 'oak-domain/lib/store/relation'; import { combineFilters } from 'oak-domain/lib/store/filter'; import { MODI_NEXT_PATH_SUFFIX } from './features/runningTree'; import { generateNewId } from 'oak-domain/lib/utils/uuid'; export function onPathSet< ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext, FrontCxt extends SyncContext>( this: ComponentFullThisType & { addFeatureSub: (name: string, callback: (args?: any) => void) => void; }, option: OakComponentOption): Partial> { const { props, state } = this; const { oakPath, oakId } = props as ComponentProps; const { entity, path, projection, isList, filters, sorters, pagination, getTotal } = option; const { features } = this; const oakPath2 = oakPath || path; assert(oakPath2); assert(!oakPath || !path); if (entity) { // entity在node生命周期中不可可变,但sorter/filter/projection应当是运行时来决定 const entity2 = entity instanceof Function ? entity.call(this) : entity; const projection2 = typeof projection === 'function' ? projection.call(this) : projection; const filters2 = filters?.map( (ele) => { const { filter, '#name': name } = ele; return { filter: typeof filter === 'function' ? () => (filter as Function).call(this) : filter, ['#name']: name, } } ); const sorters2 = sorters?.map( (ele) => { const { sorter, '#name': name } = ele; return { sorter: typeof sorter === 'function' ? () => (sorter as Function).call(this) : sorter, ['#name']: name, }; } ); assert( oakPath2, '没有正确的path信息,请检查是否配置正确' ); const { actions, cascadeActions } = option; // 在这里适配宽窄屏处理getTotal,不到运行时处理了 by Xc let getTotal2: undefined | number; if (getTotal) { const { width } = this.props; if (typeof getTotal === 'object') { const { max, deviceWidth } = getTotal; switch (deviceWidth) { case 'all': { getTotal2 = max; break; } case 'mobile': { if (width === 'xs') { getTotal2 = max; } break; } case 'pc': default: { if (width !== 'xs') { getTotal2 = max; } break; } } } else { if (width !== 'xs') { getTotal2 = getTotal; } } } features.runningTree.createNode({ path: oakPath2, entity: entity2, isList, projection: projection2, pagination: pagination, filters: filters2, sorters: sorters2, id: oakId, actions: typeof actions === 'function' ? () => actions.call(this) : actions, cascadeActions: cascadeActions && (() => cascadeActions.call(this)), getTotal: getTotal2, }); this.addFeatureSub('runningTree', (path2: string) => { // 父结点改变,子结点要重渲染 if (this.state.oakFullpath?.includes(path2)) { this.reRender(); } }); // 确保SetState生效,这里改成异步 return { oakEntity: entity2, oakFullpath: oakPath2, }; } else { // 创建virtualNode features.runningTree.createNode({ path: oakPath2 as string, }); this.addFeatureSub('runningTree', (path2: string) => { // 父结点改变,子结点要重渲染 if (this.state.oakFullpath?.includes(path2)) { this.reRender(); } }); return { oakFullpath: oakPath2, }; } } function checkActionsAndCascadeEntities< ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext, FrontCxt extends SyncContext>( this: ComponentFullThisType, rows: Partial | Partial[], option: OakComponentOption ) { const checkTypes = ['relation', 'row', 'logical', 'logicalRelation'] as CheckerType[]; const actions = this.props.oakActions ? JSON.parse(this.props.oakActions) as ED[T]['Action'][] : (typeof option.actions === 'function' ? option.actions.call(this) : option.actions); const legalActions = [] as ActionDef[]; // 这里向服务器请求相应的actionAuth,cache层会对请求加以优化,避免反复过频的不必要取数据 const destEntities: (keyof ED)[] = []; if (actions) { destEntities.push(this.state.oakEntity); // todo 这里actions整体进行测试的性能应该要高于一个个去测试 for (const action of actions as ActionDef[]) { if (rows instanceof Array) { assert(option.isList); const filter = this.features.runningTree.getIntrinsticFilters(this.state.oakFullpath!); if (action === 'create' || typeof action === 'object' && action.action === 'create') { // 创建对象的判定不落在具体行上,但要考虑list上外键相关属性的限制 const data = typeof action === 'object' && Object.assign(cloneDeep(action.data) || {}, { id: generateNewId() }); if (this.checkOperation(this.state.oakEntity, 'create', data as any, filter, checkTypes)) { legalActions.push(action); } } else { const a2 = typeof action === 'object' ? action.action : action; // 先尝试整体测试是否通过,再测试每一行 // todo,这里似乎还能优化,这些行一次性进行测试比单独测试的性能要高 if (filter && this.checkOperation(this.state.oakEntity, a2, undefined, filter, checkTypes)) { rows.forEach( (row) => { if (row['#oakLegalActions']) { row['#oakLegalActions'].push(action); } else { Object.assign(row, { '#oakLegalActions': [action], }); } } ); } else { rows.forEach( (row) => { const { id } = row; if (this.checkOperation(this.state.oakEntity, a2, undefined, { id }, checkTypes)) { if (row['#oakLegalActions']) { row['#oakLegalActions'].push(action); } else { Object.assign(row, { '#oakLegalActions': [action], }); } } } ); } } } else { assert(!option.isList); if (action === 'create' || typeof action === 'object' && action.action === 'create') { // 如果是create,根据updateData来判定。create动作应该是自动创建行的并将$$createAt$$置为1 if (rows.$$createAt$$ === 1) { const [{ operation }] = this.features.runningTree.getOperations(this.state.oakFullpath!)!; if (this.checkOperation(this.state.oakEntity, 'create', operation.data as ED[T]['Update']['data'], undefined, checkTypes)) { legalActions.push(action); if (rows['#oakLegalActions']) { rows['#oakLegalActions'].push(action); } else { Object.assign(rows, { '#oakLegalActions': [action], }); } } } } else { const a2 = typeof action === 'object' ? action.action : action; const data = typeof action === 'object' ? action.data : undefined; const filter = this.features.runningTree.getIntrinsticFilters(this.state.oakFullpath!); if (filter && this.checkOperation(this.state.oakEntity, a2, data as any, filter, checkTypes)) { legalActions.push(action); if (rows['#oakLegalActions']) { rows['#oakLegalActions'].push(action); } else { Object.assign(rows, { '#oakLegalActions': [action], }); } } } } } } const cascadeActionDict: { [K in keyof ED[T]['Schema']]?: ActionDef[]; } = this.props.oakCascadeActions ? JSON.parse(this.props.oakCascadeActions) : ((option.cascadeActions && option.cascadeActions.call(this))); if (cascadeActionDict) { const addToRow = (r: Partial, e: keyof ED[T]['Schema'], a: ActionDef) => { if (!r['#oakLegalCascadeActions']) { Object.assign(r, { '#oakLegalCascadeActions': { [e]: [a], }, }); } else if (!r['#oakLegalCascadeActions'][e]) { Object.assign(r['#oakLegalCascadeActions'], { [e]: [a], }); } else { r['#oakLegalCascadeActions'][e].push(a); } }; for (const e in cascadeActionDict) { const cascadeActions = cascadeActionDict[e]; if (cascadeActions) { const rel = judgeRelation(this.features.cache.getSchema()!, this.state.oakEntity, e); assert(rel instanceof Array, `${this.state.oakFullpath}上所定义的cascadeAction中的键值${e}不是一对多映射`); destEntities.push(rel[0]); for (const action of cascadeActions as ActionDef[]) { if (rows instanceof Array) { if (action === 'create' || typeof action === 'object' && action.action === 'create') { rows.forEach( (row) => { const intrinsticData = rel[1] ? { id: generateNewId(), [rel[1]]: row.id, } : { id: generateNewId(), entity: this.state.oakEntity, entityId: row.id }; if (typeof action === 'object') { Object.assign(intrinsticData, action.data); } if (this.checkOperation(rel[0] as any, 'create', intrinsticData as any, undefined, checkTypes)) { addToRow(row, e, action); } } ); } else { const a2 = typeof action === 'object' ? action.action : action; const filter = typeof action === 'object' ? action.filter : undefined; const intrinsticFilter = rel[1] ? { [rel[1].slice(0, rel[1].length - 2)]: this.features.runningTree.getIntrinsticFilters(this.state.oakFullpath!), } : { [this.state.oakEntity]: this.features.runningTree.getIntrinsticFilters(this.state.oakFullpath!), }; const filter2 = combineFilters(rel[0], this.features.cache.getSchema(), [filter, intrinsticFilter]); // 先尝试整体测试是否通过,再测试每一行 // todo,这里似乎还能优化,这些行一次性进行测试比单独测试的性能要高 if (this.checkOperation(rel[0] as any, a2, undefined, filter2, checkTypes)) { rows.forEach( (row) => addToRow(row, e, action) ); } else { rows.forEach( (row) => { const { id } = row; const intrinsticFilter = rel[1] ? { [rel[1]]: id, } : { entity: this.state.oakEntity, entityId: row.id }; if (typeof action === 'object') { Object.assign(intrinsticFilter, action.filter); } if (this.checkOperation(rel[0] as any, a2, undefined, intrinsticFilter as any, checkTypes)) { addToRow(row, e, action); } } ); } } } else { if (action === 'create' || typeof action === 'object' && action.action === 'create') { const intrinsticData = rel[1] ? { id: generateNewId(), [rel[1]]: rows.id, } : { id: generateNewId(), entity: this.state.oakEntity, entityId: rows.id }; if (typeof action === 'object') { Object.assign(intrinsticData, action.data); } if (this.checkOperation(rel[0] as any, 'create', intrinsticData as any, undefined, checkTypes)) { addToRow(rows, e, action); } } else { const a2 = typeof action === 'object' ? action.action : action; const filter = typeof action === 'object' ? action.filter : undefined; const intrinsticFilter = rel[1] ? { [rel[1]]: rows.id, } : { entity: this.state.oakEntity, entityId: rows.id }; const filter2 = combineFilters(rel[0], this.features.cache.getSchema(), [filter, intrinsticFilter]); // 先尝试整体测试是否通过,再测试每一行 // todo,这里似乎还能优化,这些行一次性进行测试比单独测试的性能要高 if (this.checkOperation(rel[0] as any, a2, undefined, filter2, checkTypes)) { addToRow(rows, e, action); } } } } } } } if (destEntities.length > 0) { // 权限判断需要actionAuth的数据,这里向cache请求时,会根据keepFresh规则进行一定程度的优化。 this.features.cache.refresh('actionAuth', { data: { id: 1, relationId: 1, path: { id: 1, sourceEntity: 1, destEntity: 1, value: 1, recursive: 1, }, deActions: 1, }, filter: { path: { destEntity: { $in: destEntities as string[], } } } }, undefined, undefined, { useLocalCache: { keys: destEntities as string[], gap: process.env.NODE_ENV === 'development' ? 60 * 1000 : 1200 * 1000, } }); } return legalActions; } export function reRender< ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext, FrontCxt extends SyncContext>( this: ComponentFullThisType, option: OakComponentOption, extra?: Record) { const { features } = this; const { formData } = option; const localeState = features.locales.getState(); if (this.state.oakEntity && this.state.oakFullpath) { const rows = this.features.runningTree.getFreshValue( this.state.oakFullpath ); const oakDirty = this.features.runningTree.isDirty(this.state.oakFullpath); /** * 这里的pullDownRefresh处理的应该有问题,先不动。to wangkejun. By Xc 20230201 */ const oakLoading = this.features.runningTree.isLoading(this.state.oakFullpath); const oakLoadingMore = this.features.runningTree.isLoadingMore(this.state.oakFullpath); const oakExecuting = this.features.runningTree.isExecuting(this.state.oakFullpath); const oakExecutable = !oakExecuting && this.features.runningTree.tryExecute(this.state.oakFullpath); const oakLegalActions = rows && checkActionsAndCascadeEntities.call( this as any, rows, option as any ); let data = formData ? formData.call(this, { data: rows as RowWithActions, features, props: this.props, legalActions: oakLegalActions, }) : {}; Object.assign(data, { oakLegalActions, oakLocales: localeState.dataset, oakLocalesVersion: localeState.version, oakLng: localeState.lng, oakDefaultLng: localeState.defaultLng, }); if (option.isList) { // 因为oakFilters和props里的oakFilters同名,这里只能先注掉,好像还没有组件用过 // const oakFilters = (this as ComponentFullThisType).getFilters(); // const oakSorters = (this as ComponentFullThisType).getSorters(); const oakPagination = ( this as ComponentFullThisType ).getPagination(); Object.assign(data, { // oakFilters, // oakSorters, oakPagination, }); } for (const k in data) { if (data[k] === undefined) { Object.assign(data, { [k]: null, }); } }; Object.assign(data, { oakExecutable, oakDirty, oakLoading, oakLoadingMore, oakExecuting, }); if (extra) { Object.assign(data, extra); } this.setState(data); } else { const data: Record = formData ? formData.call(this, { features, props: this.props, } as any) : {}; if (extra) { Object.assign(data, extra); } if (this.state.oakFullpath) { /** * loadingMore和pullLoading设计上有问题,暂不处理 */ const oakDirty = this.features.runningTree.isDirty(this.state.oakFullpath); const oakExecuting = this.features.runningTree.isExecuting(this.state.oakFullpath); const oakExecutable = !oakExecuting && this.features.runningTree.tryExecute(this.state.oakFullpath); const oakLoading = this.features.runningTree.isLoading(this.state.oakFullpath); Object.assign(data, { oakDirty, oakExecutable, oakExecuting, oakLoading, }); } Object.assign(data, { oakLocales: localeState.dataset, oakLocalesVersion: localeState.version, oakLng: localeState.lng, oakDefaultLng: localeState.defaultLng, __time: Date.now(), }); // 有些环境下如果传空值不触发判断 this.setState(data); } } export async function refresh< ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext, FrontCxt extends SyncContext>( this: ComponentFullThisType ) { if (this.state.oakFullpath) { await this.features.runningTree.refresh(this.state.oakFullpath); } } export async function loadMore< ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext, FrontCxt extends SyncContext>(this: ComponentFullThisType) { if (this.state.oakEntity && this.state.oakFullpath) { try { await this.features.runningTree.loadMore(this.state.oakFullpath); } catch (err) { this.setMessage({ type: 'error', content: (err as Error).message, }); } } } export async function execute< ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext, FrontCxt extends SyncContext >( this: ComponentFullThisType, action?: ED[T]['Action'], path?: string, messageProps?: boolean | MessageProps, //默认true ) { if (this.state.oakExecuting) { throw new Error('请仔细设计按钮状态,不要允许重复点击!'); } /* this.setState({ oakFocused: undefined, }); */ const fullpath = path ? path : this.state.oakFullpath; const { message } = await this.features.runningTree.execute(fullpath, action); if (messageProps !== false) { const messageData: MessageProps = { type: 'success', content: message || '操作成功', }; if (typeof messageProps === 'object') { Object.assign(messageData, messageProps); } this.setMessage(messageData); } } export function destroyNode< ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext, FrontCxt extends SyncContext>( this: ComponentFullThisType) { assert(this.state.oakFullpath); this.features.runningTree.destroyNode(this.state.oakFullpath); unset(this.state, ['oakFullpath', 'oakEntity']); }