mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-11 00:52:14 +08:00
feat: add uuid & nanoid & unitTimestamp interface (#3684)
* refactor: select & radio compoent supports multiple type of value * fix: ridio test * feat: uuid & nanoid & snowflake interface * refactor: delete snowflake * feat: nanoid field type (#3685) * refactor: add child in inheritance of tree collection (#3676) * refactor: add child in inheritance of tree collection * refactor: add child in inheritance of tree collection * style: style improve * feat: nanoid field * chore: nanoid field type map --------- Co-authored-by: katherinehhh <shunai.tang@hand-china.com> * chore: nanoid options * refactor: unixTimestamp * fix: test * refactor: unixTimestamp * refactor: unixTimestamp * refactor: locale imrove * refactor: local improve * refactor: nanoIDInput * refactor: nanoIDInput * refactor: nanoIDInput * refactor: unixTimestamp * refactor: nanoIDInput * fix: test --------- Co-authored-by: ChengLei Shao <chareice@live.com>
This commit is contained in:
parent
4015cf1c0d
commit
c7cfeec6a1
@ -40,6 +40,9 @@ import {
|
||||
UpdatedByFieldInterface,
|
||||
UrlFieldInterface,
|
||||
SortFieldInterface,
|
||||
UUIDFieldInterface,
|
||||
NanoidFieldInterface,
|
||||
UnixTimestampFieldInterface,
|
||||
} from './interfaces';
|
||||
import {
|
||||
GeneralCollectionTemplate,
|
||||
@ -155,6 +158,9 @@ export class CollectionPlugin extends Plugin {
|
||||
UpdatedByFieldInterface,
|
||||
UrlFieldInterface,
|
||||
SortFieldInterface,
|
||||
UUIDFieldInterface,
|
||||
NanoidFieldInterface,
|
||||
UnixTimestampFieldInterface,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -34,3 +34,6 @@ export * from './updatedAt';
|
||||
export * from './updatedBy';
|
||||
export * from './url';
|
||||
export * from './sort';
|
||||
export * from './uuid';
|
||||
export * from './nanoid';
|
||||
export * from './unixTimestamp';
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||
import { operators } from './properties';
|
||||
export class NanoidFieldInterface extends CollectionFieldInterface {
|
||||
name = 'nanoid';
|
||||
type = 'object';
|
||||
group = 'advanced';
|
||||
order = 0;
|
||||
title = '{{t("Nano ID")}}';
|
||||
hidden = false;
|
||||
sortable = true;
|
||||
default = {
|
||||
type: 'nanoid',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'NanoIDInput',
|
||||
},
|
||||
};
|
||||
availableTypes = ['string', 'uid'];
|
||||
properties = {
|
||||
'uiSchema.title': {
|
||||
type: 'string',
|
||||
title: '{{t("Field display name")}}',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
title: '{{t("Field name")}}',
|
||||
required: true,
|
||||
'x-disabled': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
description:
|
||||
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
|
||||
},
|
||||
customAlphabet: {
|
||||
type: 'string',
|
||||
title: '{{t("Alphabet")}}',
|
||||
default: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
title: '{{t("Length")}}',
|
||||
default: 21,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
},
|
||||
};
|
||||
filterable = {
|
||||
operators: operators.string,
|
||||
};
|
||||
titleUsable = true;
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||
import { dateTimeProps, defaultProps, operators } from './properties';
|
||||
|
||||
export class UnixTimestampFieldInterface extends CollectionFieldInterface {
|
||||
name = 'unixTimestamp';
|
||||
type = 'object';
|
||||
group = 'datetime';
|
||||
order = 1;
|
||||
title = '{{t("Unix Timestamp")}}';
|
||||
sortable = true;
|
||||
default = {
|
||||
type: 'bigInt',
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
'x-component': 'UnixTimestamp',
|
||||
'x-component-props': {
|
||||
accuracy: 'millisecond',
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
availableTypes = ['integet', 'bigInt'];
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
'uiSchema.x-component-props.accuracy': {
|
||||
type: 'string',
|
||||
title: '{{t("Accuracy")}}',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-decorator': 'FormItem',
|
||||
default: 'millisecond',
|
||||
enum: [
|
||||
{ value: 'millisecond', label: '{{t("Millisecond")}}' },
|
||||
{ value: 'second', label: '{{t("Second")}}' },
|
||||
],
|
||||
},
|
||||
};
|
||||
filterable = {
|
||||
operators: operators.number,
|
||||
};
|
||||
titleUsable = true;
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||
import { operators } from './properties';
|
||||
|
||||
export class UUIDFieldInterface extends CollectionFieldInterface {
|
||||
name = 'uuid';
|
||||
type = 'object';
|
||||
group = 'advanced';
|
||||
order = 0;
|
||||
title = '{{t("UUID")}}';
|
||||
hidden = false;
|
||||
sortable = true;
|
||||
default = {
|
||||
type: 'uuid',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
'x-validator': 'uuid',
|
||||
},
|
||||
};
|
||||
availableTypes = ['string', 'uid'];
|
||||
properties = {
|
||||
'uiSchema.title': {
|
||||
type: 'string',
|
||||
title: '{{t("Field display name")}}',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
title: '{{t("Field name")}}',
|
||||
required: true,
|
||||
'x-disabled': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
description:
|
||||
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
|
||||
},
|
||||
};
|
||||
filterable = {
|
||||
operators: operators.string,
|
||||
};
|
||||
titleUsable = true;
|
||||
}
|
@ -891,5 +891,12 @@
|
||||
"Owners": "负责人",
|
||||
"Plugin settings": "插件设置",
|
||||
"Menu": "菜单",
|
||||
"Drag and drop sorting field": "拖拽排序字段"
|
||||
"Drag and drop sorting field": "拖拽排序字段",
|
||||
"Alphabet": "字符",
|
||||
"Accuracy": "精确度",
|
||||
"Millisecond": "毫秒",
|
||||
"Second": "秒",
|
||||
"Unix Timestamp": "Unix 时间戳",
|
||||
"Field value do not meet the requirements": "字符不符合要求",
|
||||
"Field value size is": "字符长度要求"
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||
import { SchemaSettingsDateFormat } from '../../../../schema-settings/SchemaSettingsDateFormat';
|
||||
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
|
||||
|
||||
export const unixTimestampComponentFieldSettings = new SchemaSettings({
|
||||
name: 'fieldSettings:component:UnixTimestamp',
|
||||
items: [
|
||||
{
|
||||
name: 'dateDisplayFormat',
|
||||
Component: SchemaSettingsDateFormat as any,
|
||||
useComponentProps() {
|
||||
const schema = useFieldSchema();
|
||||
const { fieldSchema: tableColumnSchema } = useColumnSchema();
|
||||
const fieldSchema = tableColumnSchema || schema;
|
||||
return {
|
||||
fieldSchema,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
@ -38,6 +38,7 @@ const InternalRangePicker = connect(
|
||||
export const DatePicker = (props) => {
|
||||
const { utc = true } = useDatePickerContext();
|
||||
const value = Array.isArray(props.value) ? props.value[0] : props.value;
|
||||
console.log(value);
|
||||
props = { utc, ...props };
|
||||
return <InternalDatePicker {...props} value={value} />;
|
||||
};
|
||||
|
@ -50,5 +50,7 @@ export * from './time-picker';
|
||||
export * from './tree-select';
|
||||
export * from './upload';
|
||||
export * from './variable';
|
||||
export * from './unixTimestamp';
|
||||
export * from './nanoIDInput';
|
||||
|
||||
import './index.less';
|
||||
|
@ -0,0 +1,46 @@
|
||||
import { customAlphabet as Alphabet } from 'nanoid';
|
||||
import React, { useEffect } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { connect, mapProps, mapReadPretty, useForm } from '@formily/react';
|
||||
import { Input as AntdInput } from 'antd';
|
||||
import { ReadPretty } from '../input';
|
||||
import { useCollectionField } from '../../../data-source/collection-field/CollectionFieldProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NanoIDInput = Object.assign(
|
||||
connect(
|
||||
AntdInput,
|
||||
mapProps((props: any, field: any) => {
|
||||
const { size, customAlphabet } = useCollectionField();
|
||||
const { t } = useTranslation();
|
||||
const form = useForm();
|
||||
function isValidNanoid(value) {
|
||||
if (value.length !== size) {
|
||||
return t('Field value size is') + ` ${size}`;
|
||||
}
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (customAlphabet.indexOf(value[i]) === -1) {
|
||||
return t(`Field value do not meet the requirements`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!field.initialValue) {
|
||||
field.setInitialValue(Alphabet(customAlphabet, size)());
|
||||
}
|
||||
form.setFieldState(field.props.name, (state) => {
|
||||
state.validator = isValidNanoid;
|
||||
});
|
||||
}, []);
|
||||
return {
|
||||
...props,
|
||||
suffix: <span>{field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffix}</span>,
|
||||
};
|
||||
}),
|
||||
mapReadPretty(ReadPretty.Input),
|
||||
),
|
||||
{
|
||||
ReadPretty: ReadPretty.Input,
|
||||
},
|
||||
);
|
@ -0,0 +1 @@
|
||||
export * from './NanoIDInput';
|
@ -21,9 +21,17 @@ Radio.__ANT_RADIO = true;
|
||||
|
||||
Radio.Group = connect(
|
||||
AntdRadio.Group,
|
||||
mapProps({
|
||||
dataSource: 'options',
|
||||
}),
|
||||
mapProps(
|
||||
{
|
||||
dataSource: 'options',
|
||||
},
|
||||
(props) => {
|
||||
return {
|
||||
...props,
|
||||
value: props.value && typeof props.value !== 'boolean' ? props.value.toString() : props.value,
|
||||
};
|
||||
},
|
||||
),
|
||||
mapReadPretty((props) => {
|
||||
if (!isValid(props.value)) {
|
||||
return <div></div>;
|
||||
|
@ -9,11 +9,11 @@ import React from 'react';
|
||||
const options = [
|
||||
{
|
||||
label: '男',
|
||||
value: 1,
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 2,
|
||||
value: '2',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -8,12 +8,12 @@ import React from 'react';
|
||||
const options = [
|
||||
{
|
||||
label: '男',
|
||||
value: 1,
|
||||
value: '1',
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 2,
|
||||
value: '2',
|
||||
color: 'red',
|
||||
},
|
||||
];
|
||||
|
@ -122,7 +122,7 @@ const InternalSelect = connect(
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return v;
|
||||
return v ? v.toString() : v;
|
||||
};
|
||||
return (
|
||||
<AntdSelect
|
||||
|
@ -44,7 +44,10 @@ export function getCurrentOptions(values: string | string[], dataSource: any[],
|
||||
if (!options) return [];
|
||||
const current: Option[] = [];
|
||||
for (const value of arrValues) {
|
||||
const option = options.find((v) => v[fieldNames.value] === value) || { value, label: value };
|
||||
const option = options.find((v) => v[fieldNames.value] == value) || {
|
||||
value,
|
||||
label: value ? value.toString() : value,
|
||||
};
|
||||
current.push(option);
|
||||
}
|
||||
return current;
|
||||
|
@ -0,0 +1,49 @@
|
||||
import { connect, mapReadPretty } from '@formily/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { DatePicker } from '../date-picker';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const toValue = (value: any, accuracy) => {
|
||||
if (value) {
|
||||
return timestampToDate(value, accuracy);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
function timestampToDate(timestamp, accuracy = 'millisecond') {
|
||||
if (accuracy === 'second') {
|
||||
timestamp *= 1000; // 如果精确度是秒级,则将时间戳乘以1000转换为毫秒级
|
||||
}
|
||||
return dayjs(timestamp);
|
||||
}
|
||||
|
||||
function getTimestamp(date, accuracy = 'millisecond') {
|
||||
if (accuracy === 'second') {
|
||||
return dayjs(date).unix();
|
||||
} else {
|
||||
return dayjs(date).valueOf(); // 默认返回毫秒级时间戳
|
||||
}
|
||||
}
|
||||
|
||||
export const UnixTimestamp = connect(
|
||||
(props) => {
|
||||
const { value, onChange, accuracy } = props;
|
||||
const v = useMemo(() => toValue(value, accuracy), [value]);
|
||||
return (
|
||||
<DatePicker
|
||||
{...props}
|
||||
value={v}
|
||||
onChange={(v: any) => {
|
||||
if (onChange) {
|
||||
onChange(getTimestamp(v, accuracy));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
mapReadPretty((props) => {
|
||||
const { value, accuracy } = props;
|
||||
const v = useMemo(() => toValue(value, accuracy), [value]);
|
||||
return <DatePicker.ReadPretty {...props} value={v} />;
|
||||
}),
|
||||
);
|
@ -0,0 +1 @@
|
||||
export * from './UnixTimestamp';
|
@ -42,6 +42,7 @@ import { subformPopoverComponentFieldSettings } from '../modules/fields/componen
|
||||
import { selectComponentFieldSettings } from '../modules/fields/component/Select/selectComponentFieldSettings';
|
||||
import { subTablePopoverComponentFieldSettings } from '../modules/fields/component/SubTable/subTablePopoverComponentFieldSettings';
|
||||
import { tagComponentFieldSettings } from '../modules/fields/component/Tag/tagComponentFieldSettings';
|
||||
import { unixTimestampComponentFieldSettings } from '../modules/fields/component/UnixTimestamp/unixTimestampComponentFieldSettings';
|
||||
|
||||
export class SchemaSettingsPlugin extends Plugin {
|
||||
async load() {
|
||||
@ -90,6 +91,8 @@ export class SchemaSettingsPlugin extends Plugin {
|
||||
this.schemaSettingsManager.add(subformPopoverComponentFieldSettings);
|
||||
this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings);
|
||||
this.schemaSettingsManager.add(datePickerComponentFieldSettings);
|
||||
this.schemaSettingsManager.add(unixTimestampComponentFieldSettings);
|
||||
|
||||
this.schemaSettingsManager.add(fileManagerComponentFieldSettings);
|
||||
this.schemaSettingsManager.add(tagComponentFieldSettings);
|
||||
this.schemaSettingsManager.add(cascadeSelectComponentFieldSettings);
|
||||
|
@ -0,0 +1,39 @@
|
||||
import { mockDatabase } from '../';
|
||||
import { Database } from '../../database';
|
||||
|
||||
describe('nanoid field', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should create nanoid field type', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
autoGenId: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'nanoid',
|
||||
name: 'id',
|
||||
primaryKey: true,
|
||||
size: 21,
|
||||
customAlphabet: '1234567890abcdef',
|
||||
},
|
||||
{
|
||||
type: 'nanoid',
|
||||
name: 'id2',
|
||||
},
|
||||
],
|
||||
});
|
||||
await Test.sync();
|
||||
const test = await Test.model.create();
|
||||
expect(test.id).toHaveLength(21);
|
||||
expect(test.id2).toHaveLength(12);
|
||||
});
|
||||
});
|
@ -25,6 +25,7 @@ import { TimeFieldOptions } from './time-field';
|
||||
import { UidFieldOptions } from './uid-field';
|
||||
import { UUIDFieldOptions } from './uuid-field';
|
||||
import { VirtualFieldOptions } from './virtual-field';
|
||||
import { NanoidFieldOptions } from './nanoid-field';
|
||||
|
||||
export * from './array-field';
|
||||
export * from './belongs-to-field';
|
||||
@ -48,6 +49,7 @@ export * from './time-field';
|
||||
export * from './uid-field';
|
||||
export * from './uuid-field';
|
||||
export * from './virtual-field';
|
||||
export * from './nanoid-field';
|
||||
|
||||
export type FieldOptions =
|
||||
| BaseFieldOptions
|
||||
@ -70,6 +72,7 @@ export type FieldOptions =
|
||||
| DateFieldOptions
|
||||
| UidFieldOptions
|
||||
| UUIDFieldOptions
|
||||
| NanoidFieldOptions
|
||||
| PasswordFieldOptions
|
||||
| ContextFieldOptions
|
||||
| BelongsToFieldOptions
|
||||
|
40
packages/core/database/src/fields/nanoid-field.ts
Normal file
40
packages/core/database/src/fields/nanoid-field.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
import { customAlphabet, nanoid } from 'nanoid';
|
||||
|
||||
const DEFAULT_SIZE = 12;
|
||||
export class NanoidField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.STRING;
|
||||
}
|
||||
|
||||
init() {
|
||||
const { name, size, customAlphabet: customAlphabetOptions } = this.options;
|
||||
|
||||
this.listener = async (instance) => {
|
||||
const value = instance.get(name);
|
||||
if (!value) {
|
||||
const nanoIdFunc = customAlphabetOptions ? customAlphabet(customAlphabetOptions) : nanoid;
|
||||
instance.set(name, nanoIdFunc(size || DEFAULT_SIZE));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bind() {
|
||||
super.bind();
|
||||
this.on('beforeCreate', this.listener);
|
||||
this.on('beforeUpdate', this.listener);
|
||||
}
|
||||
|
||||
unbind() {
|
||||
super.unbind();
|
||||
this.off('beforeCreate', this.listener);
|
||||
this.off('beforeUpdate', this.listener);
|
||||
}
|
||||
}
|
||||
|
||||
export interface NanoidFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'nanoid';
|
||||
size?: number;
|
||||
customAlphabet?: string;
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
const postgres = {
|
||||
'character varying': 'string',
|
||||
varchar: 'string',
|
||||
'character varying': ['string', 'uuid', 'nanoid'],
|
||||
varchar: ['string', 'uuid', 'nanoid'],
|
||||
char: ['string', 'uuid', 'nanoid'],
|
||||
|
||||
character: 'string',
|
||||
text: 'text',
|
||||
char: 'string',
|
||||
oid: 'string',
|
||||
name: 'string',
|
||||
|
||||
@ -29,7 +30,7 @@ const postgres = {
|
||||
path: 'json',
|
||||
polygon: 'json',
|
||||
circle: 'json',
|
||||
uuid: 'string',
|
||||
uuid: 'uuid',
|
||||
};
|
||||
|
||||
const mysql = {
|
||||
@ -41,10 +42,10 @@ const mysql = {
|
||||
'tinyint unsigned': ['integer', 'boolean', 'sort'],
|
||||
'mediumint unsigned': ['integer', 'boolean', 'sort'],
|
||||
|
||||
char: 'string',
|
||||
char: ['string', 'uuid', 'nanoid'],
|
||||
varchar: ['string', 'uuid', 'nanoid'],
|
||||
date: 'date',
|
||||
time: 'time',
|
||||
varchar: 'string',
|
||||
text: 'text',
|
||||
longtext: 'text',
|
||||
int: ['integer', 'sort'],
|
||||
@ -64,7 +65,7 @@ const mysql = {
|
||||
|
||||
const sqlite = {
|
||||
text: 'text',
|
||||
varchar: 'string',
|
||||
varchar: ['string', 'uuid', 'nanoid'],
|
||||
|
||||
integer: 'integer',
|
||||
real: 'real',
|
||||
|
Loading…
Reference in New Issue
Block a user