feat(database): add sequence field type (#779)
* feat(database): add serialString field type * feat(database): add serial string type field ui (skip ci) * test(feat/database): test field options * docs: demo * fix(database): fix array table field behavior * fix(database): fix serial type interface ui * fix(database): add match logic for patterns changes * fix(database): fix serial type query last bug in mysql * refactor(database): refactor last record logic * chore: revert modification on unnecessary file * refactor(database): rename serialString type to sequence Co-authored-by: chenos <chenlinxh@gmail.com> (cherry picked from commit 32c90b4eec0438696c52ee0562fbf0e4b5af4292)
This commit is contained in:
parent
fcccacfdf4
commit
e82c1f0243
@ -25,6 +25,7 @@
|
|||||||
"antd": "~4.19.5",
|
"antd": "~4.19.5",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"cronstrue": "^2.11.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^21.6.0",
|
"i18next": "^21.6.0",
|
||||||
"json-templates": "^4.2.0",
|
"json-templates": "^4.2.0",
|
||||||
@ -38,6 +39,7 @@
|
|||||||
"react-hotkeys-hook": "^3.4.7",
|
"react-hotkeys-hook": "^3.4.7",
|
||||||
"react-i18next": "^11.15.1",
|
"react-i18next": "^11.15.1",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
|
"react-js-cron": "^1.4.0",
|
||||||
"react-quill": "^1.3.5",
|
"react-quill": "^1.3.5",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-to-print": "^2.14.7",
|
"react-to-print": "^2.14.7",
|
||||||
|
@ -7,6 +7,7 @@ export * from './createdBy';
|
|||||||
export * from './datetime';
|
export * from './datetime';
|
||||||
export * from './email';
|
export * from './email';
|
||||||
export * from './formula';
|
export * from './formula';
|
||||||
|
export * from './sequence';
|
||||||
export * from './icon';
|
export * from './icon';
|
||||||
export * from './id';
|
export * from './id';
|
||||||
export * from './input';
|
export * from './input';
|
||||||
@ -30,4 +31,3 @@ export * from './textarea';
|
|||||||
export * from './time';
|
export * from './time';
|
||||||
export * from './updatedAt';
|
export * from './updatedAt';
|
||||||
export * from './updatedBy';
|
export * from './updatedBy';
|
||||||
|
|
||||||
|
@ -0,0 +1,419 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { Button, Select } from 'antd';
|
||||||
|
import { onFieldValueChange } from '@formily/core';
|
||||||
|
import { SchemaOptionsContext, useForm, useFormEffects } from '@formily/react';
|
||||||
|
import { ArrayTable, FormButtonGroup, FormDrawer, FormLayout, Submit } from '@formily/antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
import { Cron, SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
|
||||||
|
import { IField } from './types';
|
||||||
|
import { defaultProps, operators, unique } from './properties';
|
||||||
|
|
||||||
|
function RuleTypeSelect(props) {
|
||||||
|
const compile = useCompile();
|
||||||
|
|
||||||
|
const { setValuesIn } = useForm();
|
||||||
|
const index = ArrayTable.useIndex();
|
||||||
|
|
||||||
|
useFormEffects(() => {
|
||||||
|
onFieldValueChange(`patterns.${index}.type`, (field) => {
|
||||||
|
setValuesIn(`patterns.${index}.options`, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select {...props}>
|
||||||
|
{Object.keys(RuleTypes).map(key => (
|
||||||
|
<Select.Option key={key} value={key}>{compile(RuleTypes[key].title)}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuleOptions() {
|
||||||
|
const { type, options } = ArrayTable.useRecord();
|
||||||
|
const ruleType = RuleTypes[type];
|
||||||
|
const compile = useCompile();
|
||||||
|
return (
|
||||||
|
<div className={css`
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`}>
|
||||||
|
{Object.keys(options)
|
||||||
|
.filter(key => typeof options[key] !== 'undefined')
|
||||||
|
.map(key => {
|
||||||
|
const Component = ruleType.optionRenders[key];
|
||||||
|
const { title } = ruleType.fieldset[key]
|
||||||
|
return Component
|
||||||
|
? (
|
||||||
|
<dl key={key} className={css`
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
`}>
|
||||||
|
<dt>
|
||||||
|
{compile(title)}
|
||||||
|
</dt>
|
||||||
|
<dd className={css`
|
||||||
|
margin-bottom: 0;
|
||||||
|
`}>
|
||||||
|
<Component key={key} value={options[key]} />
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RuleTypes = {
|
||||||
|
string: {
|
||||||
|
title: '{{t("Fixed text")}}',
|
||||||
|
optionRenders: {
|
||||||
|
value(options = { value: '' }) {
|
||||||
|
return <code>{options.value}</code>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fieldset: {
|
||||||
|
value: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Text content")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
integer: {
|
||||||
|
title: '{{t("Autoincrement")}}',
|
||||||
|
optionRenders: {
|
||||||
|
digits({ value }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('{{value}} Digits', { value })}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
start({ value }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Starts from {{value}}', { value })}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cycle({ value }) {
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'string',
|
||||||
|
name: 'cycle',
|
||||||
|
'x-component': 'Cron',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fieldset: {
|
||||||
|
digits: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("Digits")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-component-props': {
|
||||||
|
min: 1,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("Start from")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-component-props': {
|
||||||
|
min: 0
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
cycle: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Reset cycle")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
['x-component']({ value, onChange }) {
|
||||||
|
const shortValues = [
|
||||||
|
{ label: '不重置', value: 0 },
|
||||||
|
{ label: '每天', value: 1, cron: '0 0 * * *' },
|
||||||
|
{ label: '每周一', value: 2, cron: '0 0 * * 1' },
|
||||||
|
{ label: '每月', value: 3, cron: '0 0 1 * *' },
|
||||||
|
{ label: '每年', value: 4, cron: '0 0 1 1 *' },
|
||||||
|
{ label: '自定义', value: 5, cron: '* * * * *' }
|
||||||
|
];
|
||||||
|
const option = typeof value === 'undefined'
|
||||||
|
? shortValues[0]
|
||||||
|
: shortValues.find(item => {
|
||||||
|
return item.cron == value
|
||||||
|
}) || shortValues[5]
|
||||||
|
return (
|
||||||
|
<fieldset>
|
||||||
|
<Select value={option.value} onChange={(v) => onChange(shortValues[v].cron)}>
|
||||||
|
{shortValues.map(item => (
|
||||||
|
<Select.Option key={item.value} value={item.value}>{item.label}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{option.value === 5
|
||||||
|
? (
|
||||||
|
<Cron
|
||||||
|
value={value}
|
||||||
|
setValue={onChange}
|
||||||
|
clearButton={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
title: '{{t("Date")}}',
|
||||||
|
optionRenders: {
|
||||||
|
format(options = { value: 'YYYYMMDD' }) {
|
||||||
|
return <code>{options.value}</code>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function RuleConfigForm() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const compile = useCompile();
|
||||||
|
const schemaOptions = useContext(SchemaOptionsContext);
|
||||||
|
const form = useForm();
|
||||||
|
const { type, options } = ArrayTable.useRecord();
|
||||||
|
const index = ArrayTable.useIndex();
|
||||||
|
const ruleType = RuleTypes[type];
|
||||||
|
return ruleType?.fieldset
|
||||||
|
? (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={() => {
|
||||||
|
FormDrawer(compile(ruleType.title), () => {
|
||||||
|
return (
|
||||||
|
<FormLayout layout="vertical">
|
||||||
|
<SchemaComponentOptions scope={schemaOptions.scope} components={schemaOptions.components}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'object',
|
||||||
|
'x-component': 'fieldset',
|
||||||
|
properties: ruleType.fieldset
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
<FormDrawer.Footer>
|
||||||
|
<FormButtonGroup className={css`
|
||||||
|
justify-content: flex-end;
|
||||||
|
`}>
|
||||||
|
<Submit
|
||||||
|
onSubmit={(values) => {
|
||||||
|
return values;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Submit')}
|
||||||
|
</Submit>
|
||||||
|
</FormButtonGroup>
|
||||||
|
</FormDrawer.Footer>
|
||||||
|
</FormLayout>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.open({
|
||||||
|
initialValues: options,
|
||||||
|
})
|
||||||
|
.then((values) => {
|
||||||
|
form.setValuesIn(`patterns.${index}`, { type, options: { ...values } });
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Configure')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sequence: IField = {
|
||||||
|
name: 'sequence',
|
||||||
|
type: 'object',
|
||||||
|
group: 'advanced',
|
||||||
|
order: 2,
|
||||||
|
title: '{{t("Sequence")}}',
|
||||||
|
sortable: true,
|
||||||
|
default: {
|
||||||
|
type: 'sequence',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {
|
||||||
|
readOnly: true,
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hasDefaultValue: false,
|
||||||
|
properties: {
|
||||||
|
...defaultProps,
|
||||||
|
unique,
|
||||||
|
patterns: {
|
||||||
|
type: 'array',
|
||||||
|
title: '{{t("Sequence rules")}}',
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'ArrayTable',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sort: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Column',
|
||||||
|
'x-component-props': { width: 50, title: '', align: 'center' },
|
||||||
|
properties: {
|
||||||
|
sort: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.SortHandle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Column',
|
||||||
|
'x-component-props': { title: '{{t("Type")}}' },
|
||||||
|
// 'x-hidden': true,
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
name: 'type',
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': RuleTypeSelect
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Column',
|
||||||
|
'x-component-props': { title: '{{t("Rule content")}}' },
|
||||||
|
properties: {
|
||||||
|
options: {
|
||||||
|
type: 'object',
|
||||||
|
name: 'options',
|
||||||
|
'x-component': RuleOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
operations: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
title: '{{t("Operations")}}',
|
||||||
|
dataIndex: 'operations',
|
||||||
|
fixed: 'right',
|
||||||
|
className: css`
|
||||||
|
> *:not(:last-child){
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
|
button{
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
config: {
|
||||||
|
type: 'void',
|
||||||
|
// 'x-component': 'span',
|
||||||
|
properties: {
|
||||||
|
options: {
|
||||||
|
type: 'object',
|
||||||
|
'x-component': RuleConfigForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// configure: {
|
||||||
|
// type: 'void',
|
||||||
|
// title: '{{t("Configure")}}',
|
||||||
|
// 'x-component': 'Action.Link',
|
||||||
|
// properties: {
|
||||||
|
// drawer: {
|
||||||
|
// type: 'void',
|
||||||
|
// 'x-component': 'Action.Drawer',
|
||||||
|
// 'x-decorator': 'Form',
|
||||||
|
// 'x-decorator-props': {
|
||||||
|
// useValues: useRowOptions
|
||||||
|
// },
|
||||||
|
// title: '{{t("Configure")}}',
|
||||||
|
// properties: {
|
||||||
|
// options: {
|
||||||
|
// type: 'void',
|
||||||
|
// 'x-component': RuleConfig
|
||||||
|
// },
|
||||||
|
// actions: {
|
||||||
|
// type: 'void',
|
||||||
|
// 'x-component': 'Action.Drawer.Footer',
|
||||||
|
// properties: {
|
||||||
|
// cancel: {
|
||||||
|
// title: '{{t("Cancel")}}',
|
||||||
|
// 'x-component': 'Action',
|
||||||
|
// 'x-component-props': {
|
||||||
|
// // useAction: '{{ cm.useCancelAction }}',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// submit: {
|
||||||
|
// title: '{{t("Submit")}}',
|
||||||
|
// 'x-component': 'Action',
|
||||||
|
// 'x-component-props': {
|
||||||
|
// type: 'primary',
|
||||||
|
// async useAction() {
|
||||||
|
// const form = useForm();
|
||||||
|
// const ctx = useActionContext();
|
||||||
|
// await form.submit();
|
||||||
|
// console.log(form);
|
||||||
|
// ctx.setVisible(false);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
remove: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Remove',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
add: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Addition',
|
||||||
|
'x-component-props': {
|
||||||
|
defaultValue: { type: 'integer' }
|
||||||
|
},
|
||||||
|
title: "{{t('Add rule')}}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filterable: {
|
||||||
|
operators: operators.string,
|
||||||
|
}
|
||||||
|
};
|
@ -130,6 +130,20 @@ export default {
|
|||||||
"Password": "密码",
|
"Password": "密码",
|
||||||
"Formula": "公式",
|
"Formula": "公式",
|
||||||
"Formula description": "基于同一条记录中的其他字段计算出一个值。",
|
"Formula description": "基于同一条记录中的其他字段计算出一个值。",
|
||||||
|
"Sequence": "自动编码",
|
||||||
|
"Sequence rules": "编号规则",
|
||||||
|
"Add rule": "添加规则",
|
||||||
|
"Type": "类型",
|
||||||
|
"Autoincrement": "自增数字",
|
||||||
|
"Fixed text": "固定文本",
|
||||||
|
"Text content": "文本内容",
|
||||||
|
"Rule content": "规则内容",
|
||||||
|
"{{value}} Digits": "{{value}} 位数字",
|
||||||
|
"Digits": "位数",
|
||||||
|
"Start from": "起始于",
|
||||||
|
"Starts from {{value}}": "从 {{value}} 开始",
|
||||||
|
"Reset cycle": "重置周期",
|
||||||
|
"Operations": "操作",
|
||||||
"Choices": "选择类型",
|
"Choices": "选择类型",
|
||||||
"Checkbox": "勾选",
|
"Checkbox": "勾选",
|
||||||
"Single select": "下拉菜单(单选)",
|
"Single select": "下拉菜单(单选)",
|
||||||
|
@ -0,0 +1,224 @@
|
|||||||
|
import {
|
||||||
|
ArrayTable,
|
||||||
|
Editable,
|
||||||
|
Form,
|
||||||
|
FormButtonGroup,
|
||||||
|
FormDrawer,
|
||||||
|
FormItem,
|
||||||
|
FormLayout,
|
||||||
|
Input,
|
||||||
|
NumberPicker,
|
||||||
|
Submit
|
||||||
|
} from '@formily/antd';
|
||||||
|
import { Select } from 'antd';
|
||||||
|
import { createForm, Field, onFieldValueChange } from '@formily/core';
|
||||||
|
import { connect, createSchemaField, observer, useField, useForm, useFormEffects } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const ViewOptions = connect((props) => {
|
||||||
|
return <div>{JSON.stringify(props.value)}</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
type1: {
|
||||||
|
input1: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Input11',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
input2: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Input11',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type2: {
|
||||||
|
input1: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Input12',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
input2: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Input12',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function TypeSelect(props) {
|
||||||
|
const { setValuesIn } = useForm();
|
||||||
|
const index = ArrayTable.useIndex();
|
||||||
|
|
||||||
|
useFormEffects(() => {
|
||||||
|
onFieldValueChange(`projects.${index}.type`, (field) => {
|
||||||
|
setValuesIn(`projects.${index}.options`, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select {...props}>
|
||||||
|
{Object.keys(types).map(key => (
|
||||||
|
<Select.Option key={key} value={key}>{types[key].title}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditOptions = observer((props) => {
|
||||||
|
const record = ArrayTable.useRecord();
|
||||||
|
const field = useField<Field>();
|
||||||
|
console.log(record.type);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
FormDrawer('Pop-up form', () => {
|
||||||
|
return (
|
||||||
|
<FormLayout labelCol={6} wrapperCol={10}>
|
||||||
|
<SchemaField
|
||||||
|
schema={{
|
||||||
|
type: 'object',
|
||||||
|
properties: record.type ? types[record.type] || {} : {},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormDrawer.Footer>
|
||||||
|
<FormButtonGroup align="right">
|
||||||
|
<Submit
|
||||||
|
onSubmit={() => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Submit>
|
||||||
|
</FormButtonGroup>
|
||||||
|
</FormDrawer.Footer>
|
||||||
|
</FormLayout>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.open({
|
||||||
|
initialValues: field.value,
|
||||||
|
})
|
||||||
|
.then((values) => {
|
||||||
|
field.value = values;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const SchemaField = createSchemaField({
|
||||||
|
components: {
|
||||||
|
FormItem,
|
||||||
|
Editable,
|
||||||
|
Input,
|
||||||
|
NumberPicker,
|
||||||
|
ArrayTable,
|
||||||
|
Select,
|
||||||
|
ViewOptions,
|
||||||
|
EditOptions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = createForm();
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
projects: {
|
||||||
|
type: 'array',
|
||||||
|
title: 'Projects',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'ArrayTable',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sort: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Column',
|
||||||
|
'x-component-props': { width: 50, title: '', align: 'center' },
|
||||||
|
properties: {
|
||||||
|
sort: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.SortHandle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column_33: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
title: 'Type',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': TypeSelect
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column_3: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
title: 'Options',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
options: {
|
||||||
|
type: 'object',
|
||||||
|
'x-component': 'ViewOptions'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column_6: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
title: 'Operations',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
item: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormItem',
|
||||||
|
properties: {
|
||||||
|
options: {
|
||||||
|
type: 'object',
|
||||||
|
'x-component': 'EditOptions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
add: {
|
||||||
|
type: 'void',
|
||||||
|
title: 'Add',
|
||||||
|
'x-component': 'ArrayTable.Addition',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<SchemaField schema={schema} />
|
||||||
|
<FormButtonGroup>
|
||||||
|
<Submit onSubmit={console.log}>提交</Submit>
|
||||||
|
</FormButtonGroup>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
@ -40,3 +40,5 @@ group:
|
|||||||
### Action + Action.Popover
|
### Action + Action.Popover
|
||||||
|
|
||||||
<code src="./demos/demo4.tsx"/>
|
<code src="./demos/demo4.tsx"/>
|
||||||
|
|
||||||
|
<code src="./demos/demo5.tsx"/>
|
||||||
|
68
packages/core/client/src/schema-component/antd/cron/Cron.tsx
Normal file
68
packages/core/client/src/schema-component/antd/cron/Cron.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||||
|
import { Cron as ReactCron, CronProps } from 'react-js-cron';
|
||||||
|
import cronstrue from 'cronstrue';
|
||||||
|
import 'cronstrue/locales/zh_CN';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
import localeZhCN from './locale/zh-CN';
|
||||||
|
|
||||||
|
const ComponentLocales = {
|
||||||
|
'zh-CN': localeZhCN,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReadPrettyLocales = {
|
||||||
|
'en-US': 'en',
|
||||||
|
'zh-CN': 'zh_CN'
|
||||||
|
};
|
||||||
|
|
||||||
|
type ComposedCron = React.FC<CronProps> & {}
|
||||||
|
|
||||||
|
export const Cron: ComposedCron = connect(
|
||||||
|
(props: Exclude<CronProps, 'setValue'> & { onChange: (value: string) => void }) => {
|
||||||
|
const { onChange, ...rest } = props;
|
||||||
|
const locale = ComponentLocales[localStorage.getItem('NOCOBASE_LOCALE') || 'en-US'];
|
||||||
|
return (
|
||||||
|
<fieldset className={css`
|
||||||
|
.react-js-cron{
|
||||||
|
padding: .5em .5em 0 .5em;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
|
||||||
|
.react-js-cron-field{
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
|
||||||
|
> span{
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0 .5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .react-js-cron-select{
|
||||||
|
margin: 0 .5em 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
<ReactCron
|
||||||
|
setValue={onChange}
|
||||||
|
locale={locale}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
mapReadPretty((props) => {
|
||||||
|
const locale = ReadPrettyLocales[localStorage.getItem('NOCOBASE_LOCALE') || 'en-US'];
|
||||||
|
return props.value
|
||||||
|
? (
|
||||||
|
<span>
|
||||||
|
{cronstrue.toString(props.value, {
|
||||||
|
locale,
|
||||||
|
use24HourTimeFormat: true
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Cron;
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Cron';
|
@ -0,0 +1,79 @@
|
|||||||
|
export default {
|
||||||
|
everyText: '每',
|
||||||
|
emptyMonths: '每月',
|
||||||
|
emptyMonthDays: '每日(月)',
|
||||||
|
emptyMonthDaysShort: '每日',
|
||||||
|
emptyWeekDays: '每天(周)',
|
||||||
|
emptyWeekDaysShort: '每天(周)',
|
||||||
|
emptyHours: '每小时',
|
||||||
|
emptyMinutes: '每分钟',
|
||||||
|
emptyMinutesForHourPeriod: '每',
|
||||||
|
yearOption: '年',
|
||||||
|
monthOption: '月',
|
||||||
|
weekOption: '周',
|
||||||
|
dayOption: '天',
|
||||||
|
hourOption: '小时',
|
||||||
|
minuteOption: '分钟',
|
||||||
|
rebootOption: '重启',
|
||||||
|
prefixPeriod: '每',
|
||||||
|
prefixMonths: '的',
|
||||||
|
prefixMonthDays: '的',
|
||||||
|
prefixWeekDays: '的',
|
||||||
|
prefixWeekDaysForMonthAndYearPeriod: '并且',
|
||||||
|
prefixHours: '的',
|
||||||
|
prefixMinutes: ':',
|
||||||
|
prefixMinutesForHourPeriod: '的',
|
||||||
|
suffixMinutesForHourPeriod: '分钟',
|
||||||
|
errorInvalidCron: '不符合 cron 规则的表达式',
|
||||||
|
clearButtonText: '清空',
|
||||||
|
weekDays: [
|
||||||
|
// Order is important, the index will be used as value
|
||||||
|
'周日', // Sunday must always be first, it's "0"
|
||||||
|
'周一',
|
||||||
|
'周二',
|
||||||
|
'周三',
|
||||||
|
'周四',
|
||||||
|
'周五',
|
||||||
|
'周六',
|
||||||
|
],
|
||||||
|
months: [
|
||||||
|
// Order is important, the index will be used as value
|
||||||
|
'一月',
|
||||||
|
'二月',
|
||||||
|
'三月',
|
||||||
|
'四月',
|
||||||
|
'五月',
|
||||||
|
'六月',
|
||||||
|
'七月',
|
||||||
|
'八月',
|
||||||
|
'九月',
|
||||||
|
'十月',
|
||||||
|
'十一月',
|
||||||
|
'十二月',
|
||||||
|
],
|
||||||
|
altWeekDays: [
|
||||||
|
// Order is important, the index will be used as value
|
||||||
|
'周日', // Sunday must always be first, it's "0"
|
||||||
|
'周一',
|
||||||
|
'周二',
|
||||||
|
'周三',
|
||||||
|
'周四',
|
||||||
|
'周五',
|
||||||
|
'周六',
|
||||||
|
],
|
||||||
|
altMonths: [
|
||||||
|
// Order is important, the index will be used as value
|
||||||
|
'一月',
|
||||||
|
'二月',
|
||||||
|
'三月',
|
||||||
|
'四月',
|
||||||
|
'五月',
|
||||||
|
'六月',
|
||||||
|
'七月',
|
||||||
|
'八月',
|
||||||
|
'九月',
|
||||||
|
'十月',
|
||||||
|
'十一月',
|
||||||
|
'十二月',
|
||||||
|
],
|
||||||
|
}
|
@ -4,6 +4,7 @@ export * from './calendar';
|
|||||||
export * from './card-item';
|
export * from './card-item';
|
||||||
export * from './cascader';
|
export * from './cascader';
|
||||||
export * from './checkbox';
|
export * from './checkbox';
|
||||||
|
export * from './cron';
|
||||||
export * from './color-select';
|
export * from './color-select';
|
||||||
export * from './date-picker';
|
export * from './date-picker';
|
||||||
export * from './filter';
|
export * from './filter';
|
||||||
|
@ -50,7 +50,7 @@ export const useSchemaTemplate = () => {
|
|||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const schemaId = fieldSchema['x-uid'];
|
const schemaId = fieldSchema['x-uid'];
|
||||||
const templateKey = fieldSchema['x-template-key'];
|
const templateKey = fieldSchema['x-template-key'];
|
||||||
console.log('templateKey', { schemaId, templateKey })
|
// console.log('templateKey', { schemaId, templateKey })
|
||||||
return useMemo(() => getTemplateBySchema(fieldSchema), [schemaId, templateKey]);
|
return useMemo(() => getTemplateBySchema(fieldSchema), [schemaId, templateKey]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,10 +14,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/utils": "0.7.4-alpha.7",
|
"@nocobase/utils": "0.7.4-alpha.7",
|
||||||
"async-mutex": "^0.3.2",
|
"async-mutex": "^0.3.2",
|
||||||
|
"cron-parser": "4.4.0",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"flat": "^5.0.2",
|
"flat": "^5.0.2",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"mathjs": "^10.6.1",
|
"mathjs": "^10.6.1",
|
||||||
|
"moment": "2.x",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
"sequelize": "^6.9.0",
|
"sequelize": "^6.9.0",
|
||||||
"umzug": "^3.1.1"
|
"umzug": "^3.1.1"
|
||||||
|
@ -0,0 +1,455 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { Database } from '../../database';
|
||||||
|
import { mockDatabase } from '..';
|
||||||
|
|
||||||
|
describe('string field', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('define', () => {
|
||||||
|
it('without any pattern will throw error', async () => {
|
||||||
|
expect(() => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [{ type: 'sequence', name: 'name' }],
|
||||||
|
});
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with empty pattern will throw error', async () => {
|
||||||
|
expect(() => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [{ type: 'sequence', name: 'name', patterns: [] }],
|
||||||
|
});
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('string pattern', () => {
|
||||||
|
it('no options', async () => {
|
||||||
|
expect(() => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [{ type: 'sequence', name: 'name', patterns: [{ type: 'string' }] }],
|
||||||
|
});
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('constant', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'abc' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe('abc');
|
||||||
|
|
||||||
|
const item2 = await TestModel.create();
|
||||||
|
expect(item2.get('name')).toBe('abc');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('integer pattern', () => {
|
||||||
|
it('default start from 0, digits as 1, no cycle', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
type: 'integer'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe('0');
|
||||||
|
|
||||||
|
const item2 = await TestModel.create();
|
||||||
|
expect(item2.get('name')).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('start from 9', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
options: {
|
||||||
|
start: 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe('9');
|
||||||
|
|
||||||
|
const item2 = await TestModel.create();
|
||||||
|
expect(item2.get('name')).toBe('9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('start from 0, current set to 9', async () => {
|
||||||
|
const collection = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
type: 'integer'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
const field = collection.getField('name');
|
||||||
|
// set current option in memory
|
||||||
|
field.options.patterns[0].options = { current: 9 };
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('digits more than 1, start from 9', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
options: {
|
||||||
|
digits: 2,
|
||||||
|
start: 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe('09');
|
||||||
|
|
||||||
|
const item2 = await TestModel.create();
|
||||||
|
expect(item2.get('name')).toBe('10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cycle by day', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
options: {
|
||||||
|
cycle: '0 0 * * * *'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create({
|
||||||
|
createdAt: yesterday
|
||||||
|
});
|
||||||
|
expect(item1.get('name')).toBe('0');
|
||||||
|
|
||||||
|
const item2 = await TestModel.create({
|
||||||
|
createdAt: yesterday
|
||||||
|
});
|
||||||
|
expect(item2.get('name')).toBe('1');
|
||||||
|
|
||||||
|
const item3 = await TestModel.create();
|
||||||
|
expect(item3.get('name')).toBe('0');
|
||||||
|
|
||||||
|
const item4 = await TestModel.create();
|
||||||
|
expect(item4.get('name')).toBe('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('date pattern', () => {
|
||||||
|
it('default to current createdAt as YYYYMMDD', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'date' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const YYYYMMDD = moment(now).format('YYYYMMDD');
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe(YYYYMMDD);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('field option', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'date', options: { field: 'date' } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'date',
|
||||||
|
name: 'date'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const date = new Date(2022, 7, 1);
|
||||||
|
const YYYYMMDD = moment(date).format('YYYYMMDD');
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create({
|
||||||
|
date
|
||||||
|
});
|
||||||
|
expect(item1.get('name')).toBe(YYYYMMDD);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('format option', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'date', options: { format: 'YYYY-MM-DD' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const YYYYMMDD = moment(now).format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe(YYYYMMDD);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mixed pattern', () => {
|
||||||
|
it('all patterns', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'A' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const YYYYMMDD = moment(now).format('YYYYMMDD');
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe(`A${YYYYMMDD}0`);
|
||||||
|
|
||||||
|
const item2 = await TestModel.create();
|
||||||
|
expect(item2.get('name')).toBe(`A${YYYYMMDD}1`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changed after generated', async () => {
|
||||||
|
const testsCollection = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'A' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const YYYYMMDD = moment(now).format('YYYYMMDD');
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe(`A${YYYYMMDD}0`);
|
||||||
|
|
||||||
|
testsCollection.setField('name', {
|
||||||
|
type: 'sequence',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'A' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
// change options but no difference with default
|
||||||
|
{ type: 'integer', options: { digits: 1 } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const item2 = await TestModel.create();
|
||||||
|
expect(item2.get('name')).toBe(`A${YYYYMMDD}1`);
|
||||||
|
|
||||||
|
testsCollection.setField('name', {
|
||||||
|
type: 'sequence',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'A' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer', options: { digits: 2 } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const item3 = await TestModel.create();
|
||||||
|
expect(item3.get('name')).toBe(`A${YYYYMMDD}00`);
|
||||||
|
|
||||||
|
testsCollection.setField('name', {
|
||||||
|
type: 'sequence',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'a' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer', options: { digits: 2 } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const item4 = await TestModel.create();
|
||||||
|
expect(item4.get('name')).toBe(`a${YYYYMMDD}01`);
|
||||||
|
|
||||||
|
testsCollection.setField('name', {
|
||||||
|
type: 'sequence',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer', options: { digits: 2 } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const item5 = await TestModel.create();
|
||||||
|
expect(item5.get('name')).toBe(`${YYYYMMDD}00`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('multiple serial fields', () => {
|
||||||
|
it('2 fields', async () => {
|
||||||
|
const testsCollection = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'A' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer', options: { digits: 2, cycle: '0 0 * * *' } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'code',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'C' } },
|
||||||
|
{ type: 'integer', options: { digits: 4 }}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
const NOW = moment(now).format('YYYYMMDD');
|
||||||
|
const YESTERDAY = moment(yesterday).format('YYYYMMDD');
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create({ createdAt: yesterday });
|
||||||
|
expect(item1.get('name')).toBe(`A${YESTERDAY}00`);
|
||||||
|
expect(item1.get('code')).toBe(`C0000`);
|
||||||
|
|
||||||
|
const item2 = await TestModel.create();
|
||||||
|
expect(item2.get('name')).toBe(`A${NOW}00`);
|
||||||
|
expect(item2.get('code')).toBe(`C0001`);
|
||||||
|
|
||||||
|
testsCollection.setField('name', {
|
||||||
|
type: 'sequence',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'a' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer', options: { digits: 1 } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const item3 = await TestModel.create();
|
||||||
|
expect(item3.get('name')).toBe(`a${NOW}0`);
|
||||||
|
expect(item3.get('code')).toBe(`C0002`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -3,7 +3,7 @@ import { BaseColumnFieldOptions, Field } from './field';
|
|||||||
|
|
||||||
export class DateField extends Field {
|
export class DateField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.DATE;
|
return DataTypes.DATE(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,8 @@ import { TimeFieldOptions } from './time-field';
|
|||||||
import { UidFieldOptions } from './uid-field';
|
import { UidFieldOptions } from './uid-field';
|
||||||
import { UUIDFieldOptions } from './uuid-field';
|
import { UUIDFieldOptions } from './uuid-field';
|
||||||
import { VirtualFieldOptions } from './virtual-field';
|
import { VirtualFieldOptions } from './virtual-field';
|
||||||
import { FormulaFieldOptions } from './formula-field'
|
import { FormulaFieldOptions } from './formula-field';
|
||||||
|
import { SequenceFieldOptions } from './sequence-field';
|
||||||
|
|
||||||
export * from './array-field';
|
export * from './array-field';
|
||||||
export * from './belongs-to-field';
|
export * from './belongs-to-field';
|
||||||
@ -48,6 +49,7 @@ export * from './uid-field';
|
|||||||
export * from './uuid-field';
|
export * from './uuid-field';
|
||||||
export * from './virtual-field';
|
export * from './virtual-field';
|
||||||
export * from './formula-field';
|
export * from './formula-field';
|
||||||
|
export { SequenceField } from './sequence-field';
|
||||||
|
|
||||||
export type FieldOptions =
|
export type FieldOptions =
|
||||||
| BaseFieldOptions
|
| BaseFieldOptions
|
||||||
@ -75,4 +77,5 @@ export type FieldOptions =
|
|||||||
| BelongsToFieldOptions
|
| BelongsToFieldOptions
|
||||||
| HasOneFieldOptions
|
| HasOneFieldOptions
|
||||||
| HasManyFieldOptions
|
| HasManyFieldOptions
|
||||||
| BelongsToManyFieldOptions;
|
| BelongsToManyFieldOptions
|
||||||
|
| SequenceFieldOptions;
|
||||||
|
200
packages/core/database/src/fields/sequence-field.ts
Normal file
200
packages/core/database/src/fields/sequence-field.ts
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import { DataTypes, Transactionable } from 'sequelize';
|
||||||
|
import parser from 'cron-parser';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { escapeRegExp } from 'lodash';
|
||||||
|
|
||||||
|
import { Registry } from '@nocobase/utils';
|
||||||
|
|
||||||
|
import { Model } from '..';
|
||||||
|
import { BaseColumnFieldOptions, Field, FieldContext } from './field';
|
||||||
|
|
||||||
|
interface Pattern {
|
||||||
|
validate?(options): string | null;
|
||||||
|
generate(this: SequenceField, instance: Model, index: number): string;
|
||||||
|
getLength(options): number;
|
||||||
|
getMatcher(options): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sequencePatterns = new Registry<Pattern>();
|
||||||
|
|
||||||
|
sequencePatterns.register('string', {
|
||||||
|
validate(options) {
|
||||||
|
if (!options?.value) {
|
||||||
|
return 'options.value should be configured as a non-empty string';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
generate(instance, index) {
|
||||||
|
const { options } = this.options.patterns[index];
|
||||||
|
return options.value;
|
||||||
|
},
|
||||||
|
getLength(options) {
|
||||||
|
return options.value.length;
|
||||||
|
},
|
||||||
|
getMatcher(options) {
|
||||||
|
return escapeRegExp(options.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sequencePatterns.register('integer', {
|
||||||
|
generate(instance: Model, index) {
|
||||||
|
const { options = {} } = this.options.patterns[index];
|
||||||
|
const { digits = 1, start = 0, base = 10, cycle } = options;
|
||||||
|
const max = Math.pow(base, digits) - 1;
|
||||||
|
const { lastRecord = null } = this.options;
|
||||||
|
|
||||||
|
if (typeof options.current === 'undefined') {
|
||||||
|
if (lastRecord) {
|
||||||
|
// if match current pattern
|
||||||
|
const matcher = this.match(lastRecord.get(this.options.name));
|
||||||
|
if (matcher) {
|
||||||
|
const lastNumber = Number.parseInt(matcher[index + 1], base);
|
||||||
|
options.current = Number.isNaN(lastNumber) ? start : lastNumber + 1;
|
||||||
|
} else {
|
||||||
|
options.current = start;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options.current = start;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options.current += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cycle as cron string
|
||||||
|
if (cycle && lastRecord) {
|
||||||
|
const interval = parser.parseExpression(cycle, { currentDate: <Date>lastRecord.get('createdAt') });
|
||||||
|
const next = interval.next();
|
||||||
|
if ((<Date>instance.get('createdAt')).getTime() >= next.getTime()) {
|
||||||
|
options.current = start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.current > max) {
|
||||||
|
options.current = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update options
|
||||||
|
Object.assign(this.options.patterns[index], { options });
|
||||||
|
|
||||||
|
return options.current.toString(base).padStart(digits, '0');
|
||||||
|
},
|
||||||
|
|
||||||
|
getLength({ digits = 1 } = {}) {
|
||||||
|
return digits;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMatcher(options = {}) {
|
||||||
|
const { digits = 1, start = 0, base = 10 } = options;
|
||||||
|
const startLen = start ? start.toString(base).length : 1;
|
||||||
|
const chars = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, base);
|
||||||
|
return `[${chars}]{${digits}}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sequencePatterns.register('date', {
|
||||||
|
generate(instance, index) {
|
||||||
|
const { options } = this.options.patterns[index];
|
||||||
|
return moment(instance.get(options?.field ?? 'createdAt')).format(options?.format ?? 'YYYYMMDD');
|
||||||
|
},
|
||||||
|
getLength(options) {
|
||||||
|
return options.format?.length ?? 8;
|
||||||
|
},
|
||||||
|
getMatcher(options = {}) {
|
||||||
|
return `.{${options?.format?.length ?? 8}}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PatternConfig {
|
||||||
|
type: string;
|
||||||
|
title?: string;
|
||||||
|
options?: any;
|
||||||
|
}
|
||||||
|
export interface SequenceFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'sequence';
|
||||||
|
patterns: PatternConfig[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SequenceField extends Field {
|
||||||
|
get dataType() {
|
||||||
|
return DataTypes.STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: SequenceFieldOptions, context: FieldContext) {
|
||||||
|
super(options, context);
|
||||||
|
if (!options.patterns || !options.patterns.length) {
|
||||||
|
throw new Error('at least one pattern should be defined for sequence type');
|
||||||
|
}
|
||||||
|
options.patterns.forEach(pattern => {
|
||||||
|
const P = sequencePatterns.get(pattern.type);
|
||||||
|
if (!P) {
|
||||||
|
throw new Error(`pattern type ${pattern.type} is not registered`);
|
||||||
|
}
|
||||||
|
if (P.validate) {
|
||||||
|
const error = P.validate(pattern.options);
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const patterns = options.patterns
|
||||||
|
.map(({ type, options }) => sequencePatterns.get(type).getMatcher(options));
|
||||||
|
this.matcher = new RegExp(`^${patterns.map(p => `(${p})`).join('')}$`, 'i');
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue = async (instance: Model, options) => {
|
||||||
|
const { name, patterns } = this.options;
|
||||||
|
// NOTE: only load when value is not set, if null stand for no last record
|
||||||
|
if (typeof this.options.lastRecord === 'undefined') {
|
||||||
|
const model = <typeof Model>instance.constructor;
|
||||||
|
this.options.lastRecord = await model.findOne({
|
||||||
|
attributes: [model.primaryKeyAttribute, this.options.name, 'createdAt'],
|
||||||
|
order: [
|
||||||
|
['createdAt', 'DESC'],
|
||||||
|
// TODO(bug): will cause problem if no auto-increment id
|
||||||
|
[model.primaryKeyAttribute, 'DESC']
|
||||||
|
],
|
||||||
|
transaction: options.transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = patterns.reduce((result, p, i) => {
|
||||||
|
const item = sequencePatterns.get(p.type).generate.call(this, instance, i, options);
|
||||||
|
return result.concat(item);
|
||||||
|
}, []);
|
||||||
|
instance.set(name, results.join(''));
|
||||||
|
};
|
||||||
|
|
||||||
|
setLast = (instance: Model, options) => {
|
||||||
|
this.options.lastRecord = instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
match(value) {
|
||||||
|
return value.match(this.matcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(value: string, patternIndex: number): string {
|
||||||
|
for (let i = 0, index = 0; i < this.options.patterns.length; i += 1) {
|
||||||
|
const { type, options } = this.options.patterns[i];
|
||||||
|
const { getLength } = sequencePatterns.get(type);
|
||||||
|
const length = getLength(options);
|
||||||
|
if (i === patternIndex) {
|
||||||
|
return value.substring(index, index + length);
|
||||||
|
}
|
||||||
|
index += length;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
bind() {
|
||||||
|
super.bind();
|
||||||
|
this.on('beforeCreate', this.setValue);
|
||||||
|
this.on('afterCreate', this.setLast);
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind() {
|
||||||
|
super.unbind();
|
||||||
|
this.off('beforeCreate', this.setValue);
|
||||||
|
this.off('afterCreate', this.setLast);
|
||||||
|
}
|
||||||
|
}
|
10
yarn.lock
10
yarn.lock
@ -8625,6 +8625,11 @@ croner@~4.1.92:
|
|||||||
resolved "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz#6e373dc7bb3026fab2deb0d82685feef20796766"
|
resolved "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz#6e373dc7bb3026fab2deb0d82685feef20796766"
|
||||||
integrity sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==
|
integrity sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==
|
||||||
|
|
||||||
|
cronstrue@^2.11.0:
|
||||||
|
version "2.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.11.0.tgz#18ff1b95a836b9b4e06854f796db2dc8fa98ce41"
|
||||||
|
integrity sha512-iIBCSis5yqtFYWtJAmNOiwDveFWWIn+8uV5UYuPHYu/Aeu5CSSJepSbaHMyfc+pPFgnsCcGzfPQEo7LSGmWbTg==
|
||||||
|
|
||||||
cross-env@^7.0.3:
|
cross-env@^7.0.3:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
|
resolved "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
|
||||||
@ -15531,6 +15536,11 @@ moment-timezone@^0.5.31:
|
|||||||
dependencies:
|
dependencies:
|
||||||
moment ">= 2.9.0"
|
moment ">= 2.9.0"
|
||||||
|
|
||||||
|
moment@2.x:
|
||||||
|
version "2.29.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||||
|
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||||
|
|
||||||
"moment@>= 2.9.0", moment@^2.24.0, moment@^2.25.3, moment@^2.26.0:
|
"moment@>= 2.9.0", moment@^2.24.0, moment@^2.25.3, moment@^2.26.0:
|
||||||
version "2.29.1"
|
version "2.29.1"
|
||||||
resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||||
|
Loading…
Reference in New Issue
Block a user