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",
 | 
			
		||||
    "axios": "^0.26.1",
 | 
			
		||||
    "classnames": "^2.3.1",
 | 
			
		||||
    "cronstrue": "^2.11.0",
 | 
			
		||||
    "file-saver": "^2.0.5",
 | 
			
		||||
    "i18next": "^21.6.0",
 | 
			
		||||
    "json-templates": "^4.2.0",
 | 
			
		||||
@ -38,6 +39,7 @@
 | 
			
		||||
    "react-hotkeys-hook": "^3.4.7",
 | 
			
		||||
    "react-i18next": "^11.15.1",
 | 
			
		||||
    "react-image-lightbox": "^5.1.4",
 | 
			
		||||
    "react-js-cron": "^1.4.0",
 | 
			
		||||
    "react-quill": "^1.3.5",
 | 
			
		||||
    "react-router-dom": "^5.2.0",
 | 
			
		||||
    "react-to-print": "^2.14.7",
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ export * from './createdBy';
 | 
			
		||||
export * from './datetime';
 | 
			
		||||
export * from './email';
 | 
			
		||||
export * from './formula';
 | 
			
		||||
export * from './sequence';
 | 
			
		||||
export * from './icon';
 | 
			
		||||
export * from './id';
 | 
			
		||||
export * from './input';
 | 
			
		||||
@ -30,4 +31,3 @@ export * from './textarea';
 | 
			
		||||
export * from './time';
 | 
			
		||||
export * from './updatedAt';
 | 
			
		||||
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": "密码",
 | 
			
		||||
  "Formula": "公式",
 | 
			
		||||
  "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": "选择类型",
 | 
			
		||||
  "Checkbox": "勾选",
 | 
			
		||||
  "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
 | 
			
		||||
 | 
			
		||||
<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 './cascader';
 | 
			
		||||
export * from './checkbox';
 | 
			
		||||
export * from './cron';
 | 
			
		||||
export * from './color-select';
 | 
			
		||||
export * from './date-picker';
 | 
			
		||||
export * from './filter';
 | 
			
		||||
 | 
			
		||||
@ -50,7 +50,7 @@ export const useSchemaTemplate = () => {
 | 
			
		||||
  const fieldSchema = useFieldSchema();
 | 
			
		||||
  const schemaId = fieldSchema['x-uid'];
 | 
			
		||||
  const templateKey = fieldSchema['x-template-key'];
 | 
			
		||||
  console.log('templateKey', { schemaId, templateKey })
 | 
			
		||||
  // console.log('templateKey', { schemaId, templateKey })
 | 
			
		||||
  return useMemo(() => getTemplateBySchema(fieldSchema), [schemaId, templateKey]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -14,10 +14,12 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@nocobase/utils": "0.7.4-alpha.7",
 | 
			
		||||
    "async-mutex": "^0.3.2",
 | 
			
		||||
    "cron-parser": "4.4.0",
 | 
			
		||||
    "deepmerge": "^4.2.2",
 | 
			
		||||
    "flat": "^5.0.2",
 | 
			
		||||
    "glob": "^7.1.6",
 | 
			
		||||
    "mathjs": "^10.6.1",
 | 
			
		||||
    "moment": "2.x",
 | 
			
		||||
    "semver": "^7.3.7",
 | 
			
		||||
    "sequelize": "^6.9.0",
 | 
			
		||||
    "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 {
 | 
			
		||||
  get dataType() {
 | 
			
		||||
    return DataTypes.DATE;
 | 
			
		||||
    return DataTypes.DATE(3);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,8 @@ import { TimeFieldOptions } from './time-field';
 | 
			
		||||
import { UidFieldOptions } from './uid-field';
 | 
			
		||||
import { UUIDFieldOptions } from './uuid-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 './belongs-to-field';
 | 
			
		||||
@ -48,6 +49,7 @@ export * from './uid-field';
 | 
			
		||||
export * from './uuid-field';
 | 
			
		||||
export * from './virtual-field';
 | 
			
		||||
export * from './formula-field';
 | 
			
		||||
export { SequenceField } from './sequence-field';
 | 
			
		||||
 | 
			
		||||
export type FieldOptions =
 | 
			
		||||
  | BaseFieldOptions
 | 
			
		||||
@ -75,4 +77,5 @@ export type FieldOptions =
 | 
			
		||||
  | BelongsToFieldOptions
 | 
			
		||||
  | HasOneFieldOptions
 | 
			
		||||
  | 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"
 | 
			
		||||
  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:
 | 
			
		||||
  version "7.0.3"
 | 
			
		||||
  resolved "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
 | 
			
		||||
@ -15531,6 +15536,11 @@ moment-timezone@^0.5.31:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    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:
 | 
			
		||||
  version "2.29.1"
 | 
			
		||||
  resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user