feat: support JSONB (#2321)

* chore: yarn.lock

* feat: add jsonb option in field drawer

* feat: only postgres can use JSONB

* chore: add test

* refactor: make better

* fix: fix build

* fix: fix build

* fix: should disable JSONB on editing field
This commit is contained in:
被雨水过滤的空气-Rain 2023-07-30 09:51:39 +08:00 committed by GitHub
parent b8175dbf17
commit b22207b180
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 110 additions and 6 deletions

View File

@ -5,7 +5,15 @@ import { useRequest } from '../api-client';
export const CurrentAppInfoContext = createContext(null); export const CurrentAppInfoContext = createContext(null);
export const useCurrentAppInfo = () => { export const useCurrentAppInfo = () => {
return useContext(CurrentAppInfoContext); return useContext<{
data: {
database: {
dialect: string;
};
lang: string;
version: string;
};
}>(CurrentAppInfoContext);
}; };
export const CurrentAppInfoProvider = (props) => { export const CurrentAppInfoProvider = (props) => {
const result = useRequest({ const result = useRequest({

View File

@ -9,10 +9,11 @@ import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import useDialect from '../hooks/useDialect';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
import { getOptions } from './interfaces'; import { getOptions } from './interfaces';
@ -176,6 +177,8 @@ export const AddFieldAction = (props) => {
const [schema, setSchema] = useState({}); const [schema, setSchema] = useState({});
const compile = useCompile(); const compile = useCompile();
const { t } = useTranslation(); const { t } = useTranslation();
const { isDialect } = useDialect();
const currentCollections = useMemo(() => { const currentCollections = useMemo(() => {
return collections.map((v) => { return collections.map((v) => {
return { return {
@ -298,6 +301,8 @@ export const AddFieldAction = (props) => {
showReverseFieldConfig: true, showReverseFieldConfig: true,
targetScope, targetScope,
collections: currentCollections, collections: currentCollections,
isDialect,
disabledJSONB: false,
...scope, ...scope,
}} }}
/> />

View File

@ -8,10 +8,11 @@ import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction, useUpdateAction } from '../action-hooks'; import { useCancelAction, useUpdateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import useDialect from '../hooks/useDialect';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
@ -144,6 +145,8 @@ export const EditFieldAction = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const compile = useCompile(); const compile = useCompile();
const [data, setData] = useState<any>({}); const [data, setData] = useState<any>({});
const { isDialect } = useDialect();
const currentCollections = useMemo(() => { const currentCollections = useMemo(() => {
return collections.map((v) => { return collections.map((v) => {
return { return {
@ -194,6 +197,8 @@ export const EditFieldAction = (props) => {
useCancelAction, useCancelAction,
showReverseFieldConfig: !data?.reverseField, showReverseFieldConfig: !data?.reverseField,
collections: currentCollections, collections: currentCollections,
isDialect,
disabledJSONB: true,
...scope, ...scope,
}} }}
/> />

View File

@ -0,0 +1,16 @@
import { useCurrentAppInfo } from '../../appInfo';
const useDialect = () => {
const {
data: { database },
} = useCurrentAppInfo();
const isDialect = (dialect: string) => database?.dialect === dialect;
return {
isDialect,
dialect: database?.dialect,
};
};
export default useDialect;

View File

@ -0,0 +1,41 @@
import React from 'react';
import { render, screen, waitFor } from 'testUtils';
import { CurrentAppInfoContext } from '../../../appInfo';
import { Checkbox } from '../../../schema-component/antd/checkbox';
import { Input } from '../../../schema-component/antd/input';
import { SchemaComponent } from '../../../schema-component/core/SchemaComponent';
import { SchemaComponentProvider } from '../../../schema-component/core/SchemaComponentProvider';
import { json } from '../json';
const Component = () => {
return (
<SchemaComponentProvider components={{ Input, Checkbox }}>
<SchemaComponent schema={json} />
</SchemaComponentProvider>
);
};
// TODO: 需要先修复测试中的路径问题:即某些引用路径返回的模块是 undefined
describe('JSON', () => {
it('should show JSONB when dialect is postgres', async () => {
render(<Component />, {
wrapper: ({ children }) => (
<CurrentAppInfoContext.Provider
value={{
data: {
database: {
dialect: 'postgres',
},
},
}}
>
{children}
</CurrentAppInfoContext.Provider>
),
});
await waitFor(() => {
expect(screen.queryByText('JSONB')).toBeInTheDocument();
});
});
});

View File

@ -1,4 +1,6 @@
import { FormItem, FormLayout } from '@formily/antd-v5';
import { registerValidateRules } from '@formily/core'; import { registerValidateRules } from '@formily/core';
import React from 'react';
import { defaultProps } from './properties'; import { defaultProps } from './properties';
import { IField } from './types'; import { IField } from './types';
@ -43,6 +45,19 @@ export const json: IField = {
hasDefaultValue: true, hasDefaultValue: true,
properties: { properties: {
...defaultProps, ...defaultProps,
jsonb: {
type: 'boolean',
title: 'JSONB',
// 不直接用 `FormItem` 的原因是为了想要设置 `FormLayout` 的 `layout` 属性为 `horizontal` (默认就是 horizontal
'x-decorator': ({ children }) => (
<FormLayout>
<FormItem>{children}</FormItem>
</FormLayout>
),
'x-component': 'Checkbox',
'x-hidden': `{{ !isDialect('postgres') }}`,
'x-disabled': `{{ disabledJSONB }}`,
},
}, },
filterable: {}, filterable: {},
}; };

View File

@ -1,5 +1,5 @@
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react'; import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { lodash } from '@nocobase/utils'; import { lodash } from '@nocobase/utils/client';
import { App, Button, Popover } from 'antd'; import { App, Button, Popover } from 'antd';
import classnames from 'classnames'; import classnames from 'classnames';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';

View File

@ -3,6 +3,11 @@ import { BaseColumnFieldOptions, Field } from './field';
export class JsonField extends Field { export class JsonField extends Field {
get dataType() { get dataType() {
const dialect = this.context.database.sequelize.getDialect();
const { jsonb } = this.options;
if (dialect === 'postgres' && jsonb) {
return DataTypes.JSONB;
}
return DataTypes.JSON; return DataTypes.JSON;
} }
} }

View File

@ -59,7 +59,9 @@ const Entity: React.FC<{
}, },
id, id,
} = node; } = node;
const database = useCurrentAppInfo(); const {
data: { database },
} = useCurrentAppInfo();
const collectionData = useRef(); const collectionData = useRef();
const categoryData = useContext(CollectionCategroriesContext); const categoryData = useContext(CollectionCategroriesContext);
collectionData.current = { ...item, title, inherits: item.inherits && new Proxy(item.inherits, {}) }; collectionData.current = { ...item, title, inherits: item.inherits && new Proxy(item.inherits, {}) };
@ -184,7 +186,9 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
const [collapse, setCollapse] = useState(false); const [collapse, setCollapse] = useState(false);
const { t } = useGCMTranslation(); const { t } = useGCMTranslation();
const compile = useCompile(); const compile = useCompile();
const database = useCurrentAppInfo(); const {
data: { database },
} = useCurrentAppInfo();
const portsData = lodash.groupBy(ports.items, (v) => { const portsData = lodash.groupBy(ports.items, (v) => {
if ( if (
v.isForeignKey || v.isForeignKey ||

View File

@ -20595,6 +20595,11 @@ prettier@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz#e7b19f691245a21d618c68bc54dc06122f6105ae" resolved "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz#e7b19f691245a21d618c68bc54dc06122f6105ae"
prettier@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/prettier/-/prettier-3.0.0.tgz#e7b19f691245a21d618c68bc54dc06122f6105ae"
integrity sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==
pretty-error@^4.0.0: pretty-error@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmmirror.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" resolved "https://registry.npmmirror.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6"