amis
Version:
一种MIS页面生成工具
889 lines (784 loc) • 23.9 kB
text/typescript
import {
types,
getParent,
SnapshotIn,
flow,
getRoot,
hasParent,
isAlive,
getEnv,
Instance
} from 'mobx-state-tree';
import {IFormStore} from './form';
import {str2rules, validate as doValidate} from '../utils/validations';
import {Api, Payload, fetchOptions} from '../types';
import {ComboStore, IComboStore, IUniqueGroup} from './combo';
import {evalExpression} from '../utils/tpl';
import findIndex from 'lodash/findIndex';
import {
isArrayChildrenModified,
isObject,
createObject,
isObjectShallowModified,
findTree,
findTreeIndex,
spliceTree,
isEmpty,
getTreeAncestors,
filterTree
} from '../utils/helper';
import {flattenTree} from '../utils/helper';
import {IRendererStore} from '.';
import {normalizeOptions, optionValueCompare} from '../components/Select';
import find from 'lodash/find';
import {SimpleMap} from '../utils/SimpleMap';
import memoize from 'lodash/memoize';
import {TranslateFn} from '../locale';
import {StoreNode} from './node';
import {dataMapping} from '../utils/tpl-builtin';
import {getStoreById} from './manager';
interface IOption {
value?: string | number | null;
label?: string | null;
children?: IOption[] | null;
disabled?: boolean | null;
visible?: boolean | null;
hidden?: boolean | null;
}
const ErrorDetail = types.model('ErrorDetail', {
msg: '',
tag: ''
});
export const FormItemStore = StoreNode.named('FormItemStore')
.props({
isFocused: false,
type: '',
unique: false,
loading: false,
required: false,
tmpValue: types.frozen(),
rules: types.optional(types.frozen(), {}),
messages: types.optional(types.frozen(), {}),
errorData: types.optional(types.array(ErrorDetail), []),
name: types.string,
itemId: '', // 因为 name 可能会重名,所以加个 id 进来,如果有需要用来定位具体某一个
unsetValueOnInvisible: false,
itemsRef: types.optional(types.array(types.string), []),
validated: false,
validating: false,
multiple: false,
delimiter: ',',
valueField: 'value',
labelField: 'label',
joinValues: true,
extractValue: false,
options: types.optional(types.frozen<Array<any>>(), []),
expressionsInOptions: false,
selectFirst: false,
autoFill: types.frozen(),
clearValueOnHidden: false,
selectedOptions: types.optional(types.frozen(), []),
filteredOptions: types.optional(types.frozen(), []),
dialogSchema: types.frozen(),
dialogOpen: false,
dialogData: types.frozen(),
resetValue: types.optional(types.frozen(), '')
})
.views(self => {
function getForm(): any {
return self.parentStore;
}
function getValue(): any {
return getForm()?.getValueByName(self.name);
}
function getLastOptionValue(): any {
if (self.selectedOptions.length) {
return self.selectedOptions[self.selectedOptions.length - 1].value;
}
return '';
}
function getErrors(): Array<string> {
return self.errorData.map(item => item.msg);
}
return {
get subFormItems(): any {
return self.itemsRef.map(item => getStoreById(item));
},
get form(): any {
return getForm();
},
get value(): any {
return getValue();
},
get prinstine(): any {
return (getForm() as IFormStore).getPristineValueByName(self.name);
},
get errors() {
return getErrors();
},
get valid() {
const errors = getErrors();
return !!(!errors || !errors.length);
},
get lastSelectValue(): string {
return getLastOptionValue();
},
getSelectedOptions: (value: any = getValue()) => {
if (typeof value === 'undefined') {
return [];
}
const valueArray = Array.isArray(value)
? value
: typeof value === 'string'
? value.split(self.delimiter || ',')
: [value];
const selected = valueArray.map(item =>
item && item.hasOwnProperty(self.valueField || 'value')
? item[self.valueField || 'value']
: item
);
const selectedOptions: Array<any> = [];
selected.forEach((item, index) => {
const matched = findTree(
self.filteredOptions,
optionValueCompare(item, self.valueField || 'value')
);
if (matched) {
selectedOptions.push(matched);
} else {
let unMatched = (valueArray && valueArray[index]) || item;
if (
unMatched &&
(typeof unMatched === 'string' || typeof unMatched === 'number')
) {
unMatched = {
[self.valueField || 'value']: item,
[self.labelField || 'label']: item,
__unmatched: true
};
} else if (unMatched && self.extractValue) {
unMatched = {
[self.valueField || 'value']: item,
[self.labelField || 'label']: 'UnKnown',
__unmatched: true
};
}
unMatched && selectedOptions.push(unMatched);
}
});
return selectedOptions;
}
};
})
.actions(self => {
const form = self.form as IFormStore;
const dialogCallbacks = new SimpleMap<(result?: any) => void>();
function config({
required,
unique,
value,
rules,
messages,
delimiter,
multiple,
valueField,
labelField,
joinValues,
extractValue,
type,
id,
selectFirst,
autoFill,
clearValueOnHidden
}: {
required?: boolean;
unique?: boolean;
value?: any;
rules?: string | {[propName: string]: any};
messages?: {[propName: string]: string};
multiple?: boolean;
delimiter?: string;
valueField?: string;
labelField?: string;
joinValues?: boolean;
extractValue?: boolean;
type?: string;
id?: string;
selectFirst?: boolean;
autoFill?: any;
clearValueOnHidden?: boolean;
}) {
if (typeof rules === 'string') {
rules = str2rules(rules);
}
typeof type !== 'undefined' && (self.type = type);
typeof id !== 'undefined' && (self.itemId = id);
typeof messages !== 'undefined' && (self.messages = messages);
typeof required !== 'undefined' && (self.required = !!required);
typeof unique !== 'undefined' && (self.unique = !!unique);
typeof multiple !== 'undefined' && (self.multiple = !!multiple);
typeof selectFirst !== 'undefined' && (self.selectFirst = !!selectFirst);
typeof autoFill !== 'undefined' && (self.autoFill = autoFill);
typeof joinValues !== 'undefined' && (self.joinValues = !!joinValues);
typeof extractValue !== 'undefined' &&
(self.extractValue = !!extractValue);
typeof delimiter !== 'undefined' &&
(self.delimiter = (delimiter as string) || ',');
typeof valueField !== 'undefined' &&
(self.valueField = (valueField as string) || 'value');
typeof labelField !== 'undefined' &&
(self.labelField = (labelField as string) || 'label');
typeof clearValueOnHidden !== 'undefined' &&
(self.clearValueOnHidden = !!clearValueOnHidden);
rules = rules || {};
rules = {
...rules,
isRequired: self.required
};
if (isObjectShallowModified(rules, self.rules)) {
self.rules = rules;
clearError('builtin');
self.validated = false;
}
if (value !== void 0 && self.value === void 0) {
form.setValueByName(self.name, value, true);
syncAutoFill(value, true);
}
}
function focus() {
self.isFocused = true;
}
function blur() {
self.isFocused = false;
}
function changeValue(value: any, isPrintine: boolean = false) {
if (typeof value === 'undefined' || value === '__undefined') {
self.form.deleteValueByName(self.name);
} else {
self.form.setValueByName(self.name, value, isPrintine);
}
syncAutoFill(value, isPrintine);
}
const validate: (hook?: any) => Promise<boolean> = flow(function* validate(
hook?: any
) {
if (self.validating) {
return self.valid;
}
self.validating = true;
clearError();
if (hook) {
yield hook();
}
addError(
doValidate(
self.value,
self.form.data,
self.rules,
self.messages,
self.__
)
);
self.validated = true;
if (
self.unique &&
self.form.parentStore &&
self.form.parentStore.storeType === 'ComboStore'
) {
const combo = self.form.parentStore as IComboStore;
const group = combo.uniques.get(self.name) as IUniqueGroup;
if (
group.items.some(
item => item !== self && self.value && item.value === self.value
)
) {
addError(self.__('`当前值不唯一`'));
}
}
self.validating = false;
return self.valid;
});
function setError(msg: string | Array<string>, tag: string = 'builtin') {
clearError();
addError(msg, tag);
}
function addError(msg: string | Array<string>, tag: string = 'builtin') {
const msgs: Array<string> = Array.isArray(msg) ? msg : [msg];
msgs.forEach(item =>
self.errorData.push({
msg: item,
tag: tag
})
);
}
function clearError(tag?: string) {
if (tag) {
const filtered = self.errorData.filter(item => item.tag !== tag);
self.errorData.replace(filtered);
} else {
self.errorData.clear();
}
}
function getFirstAvaibleOption(options: Array<any>): any {
if (!Array.isArray(options)) {
return;
}
for (let option of options) {
if (Array.isArray(option.children)) {
const childFirst = getFirstAvaibleOption(option.children);
if (childFirst !== undefined) {
return childFirst;
}
} else if (option[self.valueField || 'value'] && !option.disabled) {
return option;
}
}
}
function setOptions(
options: Array<object>,
onChange?: (value: any) => void
) {
if (!Array.isArray(options)) {
return;
}
options = filterTree(options, item => item);
const originOptions = self.options.concat();
self.options = options;
syncOptions(originOptions);
let selectedOptions;
let skipAyncAutoFill = false;
if (
self.selectFirst &&
self.filteredOptions.length &&
(selectedOptions = self.getSelectedOptions(self.value)) &&
!selectedOptions.filter(item => !item.__unmatched).length
) {
const fistOption = getFirstAvaibleOption(self.filteredOptions);
if (!fistOption) {
return;
}
const list = [fistOption].map((item: any) => {
if (self.extractValue || self.joinValues) {
return item[self.valueField || 'value'];
}
return item;
});
const value =
self.joinValues && self.multiple
? list.join(self.delimiter)
: self.multiple
? list
: list[0];
if (form.inited && onChange) {
onChange(value);
} else {
changeValue(value, !form.inited);
skipAyncAutoFill = true; // changeValue 里面本来就会调用 syncAutoFill 所以跳过
}
}
skipAyncAutoFill || syncAutoFill(self.value, !form.inited);
}
let loadCancel: Function | null = null;
const fetchOptions: (
api: Api,
data?: object,
config?: fetchOptions,
setErrorFlag?: boolean
) => Promise<Payload | null> = flow(function* getInitData(
api: string,
data: object,
config?: fetchOptions,
setErrorFlag?: boolean
) {
try {
if (loadCancel) {
loadCancel();
loadCancel = null;
self.loading = false;
}
if (!config?.silent) {
self.loading = true;
}
const json: Payload = yield getEnv(self).fetcher(api, data, {
autoAppend: false,
cancelExecutor: (executor: Function) => (loadCancel = executor),
...config
});
loadCancel = null;
let result: any = null;
if (!json.ok) {
setErrorFlag !== false &&
setError(
self.__('Form.loadOptionsFailed', {
reason: json.msg ?? (config && config.errorMessage)
})
);
getEnv(self).notify(
'error',
self.errors.join('') || `${api}:${json.msg}`,
json.msgTimeout !== undefined
? {
closeButton: true,
timeout: json.msgTimeout
}
: undefined
);
} else {
result = json;
}
self.loading = false;
return result;
} catch (e) {
const env = getEnv(self);
if (!isAlive(self) || self.disposed) {
return;
}
self.loading = false;
if (env.isCancel(e)) {
return;
}
console.error(e.stack);
env.notify('error', e.message);
return;
}
} as any);
const loadOptions: (
api: Api,
data?: object,
config?: fetchOptions,
clearValue?: boolean,
onChange?: (value: any) => void,
setErrorFlag?: boolean
) => Promise<Payload | null> = flow(function* getInitData(
api: string,
data: object,
config?: fetchOptions,
clearValue?: any,
onChange?: (
value: any,
submitOnChange?: boolean,
changeImmediately?: boolean
) => void,
setErrorFlag?: boolean
) {
let json = yield fetchOptions(api, data, config, setErrorFlag);
if (!json) {
return;
}
clearError();
self.validated = false; // 拉完数据应该需要再校验一下
let options: Array<IOption> =
json.data?.options ||
json.data.items ||
json.data.rows ||
json.data ||
[];
options = normalizeOptions(options as any);
setOptions(options, onChange);
if (json.data && typeof (json.data as any).value !== 'undefined') {
onChange && onChange((json.data as any).value, false, true);
} else if (clearValue && !self.selectFirst) {
self.selectedOptions.some((item: any) => item.__unmatched) &&
onChange &&
onChange('', false, true);
}
return json;
});
const deferLoadOptions: (
option: any,
api: Api,
data?: object,
config?: fetchOptions
) => Promise<Payload | null> = flow(function* getInitData(
option: any,
api: string,
data: object,
config?: fetchOptions
) {
const indexes = findTreeIndex(self.options, item => item === option);
if (!indexes) {
return;
}
setOptions(
spliceTree(self.options, indexes, 1, {
...option,
loading: true
})
);
let json = yield fetchOptions(
api,
data,
{
...config,
silent: true
},
false
);
if (!json) {
setOptions(
spliceTree(self.options, indexes, 1, {
...option,
loading: false,
error: true
})
);
return;
}
let options: Array<IOption> =
json.data?.options ||
json.data.items ||
json.data.rows ||
json.data ||
[];
setOptions(
spliceTree(self.options, indexes, 1, {
...option,
loading: false,
loaded: true,
children: options
})
);
return json;
});
function syncOptions(originOptions?: Array<any>) {
if (!self.options.length && typeof self.value === 'undefined') {
self.selectedOptions = [];
self.filteredOptions = [];
return;
}
const form = self.form;
const value = self.value;
// 有可能销毁了
if (!form) {
return;
}
const selected = Array.isArray(value)
? value.map(item =>
item && item.hasOwnProperty(self.valueField || 'value')
? item[self.valueField || 'value']
: item
)
: typeof value === 'string'
? value.split(self.delimiter || ',')
: value === void 0
? []
: [
value && value.hasOwnProperty(self.valueField || 'value')
? value[self.valueField || 'value']
: value
];
if (value && value.hasOwnProperty(self.labelField || 'label')) {
selected[0] = {
[self.labelField || 'label']: value[self.labelField || 'label'],
[self.valueField || 'value']: value[self.valueField || 'value']
};
}
let expressionsInOptions = false;
let filteredOptions = self.options
.filter((item: any) => {
if (!expressionsInOptions && (item.visibleOn || item.hiddenOn)) {
expressionsInOptions = true;
}
return item.visibleOn
? evalExpression(item.visibleOn, form.data) !== false
: item.hiddenOn
? evalExpression(item.hiddenOn, form.data) !== true
: item.visible !== false || item.hidden !== true;
})
.map((item: any, index) => {
const disabled = evalExpression(item.disabledOn, form.data);
const newItem = item.disabledOn
? self.filteredOptions.length > index &&
self.filteredOptions[index].disabled === disabled
? self.filteredOptions[index]
: {
...item,
disabled: disabled
}
: item;
return newItem;
});
self.expressionsInOptions = expressionsInOptions;
const flattened: Array<any> = flattenTree(filteredOptions);
const selectedOptions: Array<any> = [];
selected.forEach((item, index) => {
let idx = findIndex(
flattened,
optionValueCompare(item, self.valueField || 'value')
);
if (~idx) {
selectedOptions.push(flattened[idx]);
} else {
let unMatched = (value && value[index]) || item;
if (
unMatched &&
(typeof unMatched === 'string' || typeof unMatched === 'number')
) {
unMatched = {
[self.valueField || 'value']: item,
[self.labelField || 'label']: item,
__unmatched: true
};
const orgin: any =
originOptions &&
find(
originOptions,
optionValueCompare(item, self.valueField || 'value')
);
if (orgin) {
unMatched[self.labelField || 'label'] =
orgin[self.labelField || 'label'];
}
} else if (unMatched && self.extractValue) {
unMatched = {
[self.valueField || 'value']: item,
[self.labelField || 'label']: 'UnKnown',
__unmatched: true
};
}
unMatched && selectedOptions.push(unMatched);
}
});
let parentStore = form.parentStore;
if (parentStore && parentStore.storeType === ComboStore.name) {
let combo = parentStore as IComboStore;
let group = combo.uniques.get(self.name) as IUniqueGroup;
let options: Array<any> = [];
group &&
group.items.forEach(item => {
if (self !== item) {
options.push(
...item.selectedOptions.map((item: any) => item && item.value)
);
}
});
if (filteredOptions.length) {
filteredOptions = filteredOptions.filter(
option => !~options.indexOf(option.value)
);
}
}
isArrayChildrenModified(self.selectedOptions, selectedOptions) &&
(self.selectedOptions = selectedOptions);
isArrayChildrenModified(self.filteredOptions, filteredOptions) &&
(self.filteredOptions = filteredOptions);
}
function setLoading(value: boolean) {
self.loading = value;
}
let subStore: any;
function getSubStore() {
return subStore;
}
function setSubStore(store: any) {
subStore = store;
}
function reset() {
self.validated = false;
if (subStore && subStore.storeType === 'ComboStore') {
const combo = subStore as IComboStore;
combo.forms.forEach(form => form.reset());
}
clearError();
}
function openDialog(
schema: any,
data: any = form.data,
callback?: (ret?: any) => void
) {
self.dialogSchema = schema;
self.dialogData = data;
self.dialogOpen = true;
callback && dialogCallbacks.set(self.dialogData, callback);
}
function closeDialog(result?: any) {
const callback = dialogCallbacks.get(self.dialogData);
self.dialogOpen = false;
if (callback) {
dialogCallbacks.delete(self.dialogData);
setTimeout(() => callback(result), 200);
}
}
function syncAutoFill(
value: any = self.value,
isPrintine: boolean = false
) {
if (self.autoFill && !isEmpty(self.autoFill) && self.options.length) {
const selectedOptions = self.getSelectedOptions(value);
const toSync = dataMapping(
self.autoFill,
self.multiple
? {
items: selectedOptions.map(item =>
createObject(
{
ancestors: getTreeAncestors(
self.filteredOptions,
item,
true
)
},
item
)
)
}
: createObject(
{
ancestors: getTreeAncestors(
self.filteredOptions,
selectedOptions[0],
true
)
},
selectedOptions[0]
)
);
Object.keys(toSync).forEach(key => {
const value = toSync[key];
if (typeof value === 'undefined' || value === '__undefined') {
self.form.deleteValueByName(key);
} else {
self.form.setValueByName(key, value, isPrintine);
}
});
}
}
function changeTmpValue(value: any) {
self.tmpValue = value;
}
function addSubFormItem(item: IFormItemStore) {
self.itemsRef.push(item.id);
}
function removeSubFormItem(item: IFormItemStore) {
const idx = self.itemsRef.findIndex(a => a === item.id);
if (~idx) {
self.itemsRef.splice(idx, 1);
}
}
return {
focus,
blur,
config,
changeValue,
validate,
setError,
addError,
clearError,
setOptions,
loadOptions,
deferLoadOptions,
syncOptions,
setLoading,
setSubStore,
getSubStore,
reset,
openDialog,
closeDialog,
syncAutoFill,
changeTmpValue,
addSubFormItem,
removeSubFormItem
};
});
export type IFormItemStore = Instance<typeof FormItemStore>;
export type SFormItemStore = SnapshotIn<typeof FormItemStore>;