feat: refactor mobile components and add extend collection in form (#1259)

Reviewed-on: daoyoucloud/tachybase#1259
This commit is contained in:
sealday 2024-07-02 17:49:16 +08:00
parent 62ee8730b0
commit a15f5aedf0
15 changed files with 178 additions and 302 deletions

View File

@ -0,0 +1,49 @@
import React from 'react';
import { Plugin } from '../../application/Plugin';
import { CollectionOptions, ExtendCollectionsProvider } from '../../data-source';
export class PluginBuiltInCollections extends Plugin {
async load() {
this.app.use(BuiltInCollectionsProvider);
}
}
export const collection: CollectionOptions = {
name: 'test-test',
title: 'TestTest',
fields: [
{
type: 'string',
name: 'name',
interface: 'input',
uiSchema: {
title: 'Name',
type: 'string',
'x-component': 'Input',
required: true,
},
},
{
type: 'boolean',
name: 'enabled',
interface: 'radioGroup',
uiSchema: {
title: 'Enabled',
type: 'string',
required: true,
enum: [
{ label: 'On', value: true },
{ label: 'Off', value: false },
],
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
default: false,
},
},
],
};
const BuiltInCollectionsProvider = ({ children }) => {
return <ExtendCollectionsProvider collections={[collection]}>{children}</ExtendCollectionsProvider>;
};

View File

@ -22,6 +22,7 @@ import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user'; import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user';
import { ACLPlugin } from './acl'; import { ACLPlugin } from './acl';
import { AdminLayoutPlugin } from './admin-layout'; import { AdminLayoutPlugin } from './admin-layout';
import { PluginBuiltInCollections } from './built-in-collections';
import { RemoteDocumentTitlePlugin } from './document-title'; import { RemoteDocumentTitlePlugin } from './document-title';
import { LocalePlugin } from './locale/LocalePlugin'; import { LocalePlugin } from './locale/LocalePlugin';
import { PinnedListPlugin } from './pinned-list'; import { PinnedListPlugin } from './pinned-list';

View File

@ -1,6 +1,7 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect } from 'react';
import { connect, Field, merge, useField, useFieldSchema } from '@tachybase/schema'; import { connect, Field, merge, useField, useFieldSchema } from '@tachybase/schema';
import { useDeepCompareEffect } from 'ahooks';
import { concat } from 'lodash'; import { concat } from 'lodash';
import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
@ -41,7 +42,7 @@ export const CollectionFieldInternalField = (props: Props) => {
ctx.field.added.add(fieldSchema.name); ctx.field.added.add(fieldSchema.name);
} }
}); });
useEffect(() => { useDeepCompareEffect(() => {
if (!uiSchema) { if (!uiSchema) {
return; return;
} }
@ -69,7 +70,6 @@ export const CollectionFieldInternalField = (props: Props) => {
const originalProps = compile(uiSchema['x-component-props']) || {}; const originalProps = compile(uiSchema['x-component-props']) || {};
const componentProps = merge(originalProps, field.componentProps || {}); const componentProps = merge(originalProps, field.componentProps || {});
field.component = [Component, componentProps]; field.component = [Component, componentProps];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uiSchema]); }, [uiSchema]);
if (!uiSchema) { if (!uiSchema) {
return null; return null;

View File

@ -1,4 +1,4 @@
import React, { createContext, FC, ReactNode, useContext, useMemo } from 'react'; import React, { createContext, ReactNode, useContext, useMemo } from 'react';
import { useDataSourceKey } from '../data-source/DataSourceProvider'; import { useDataSourceKey } from '../data-source/DataSourceProvider';
import { CollectionOptions } from './Collection'; import { CollectionOptions } from './Collection';
@ -12,7 +12,7 @@ export interface ExtendCollectionsProviderProps {
children?: ReactNode; children?: ReactNode;
} }
export const ExtendCollectionsProvider: FC<ExtendCollectionsProviderProps> = ({ children, collections }) => { export const ExtendCollectionsProvider = ({ children, collections }: ExtendCollectionsProviderProps) => {
const parentCollections = useExtendCollections(); const parentCollections = useExtendCollections();
const extendCollections = useMemo(() => { const extendCollections = useMemo(() => {
return parentCollections ? [...parentCollections, ...collections] : collections; return parentCollections ? [...parentCollections, ...collections] : collections;

View File

@ -1,5 +1,9 @@
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer'; import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { AssociatedFields, ParentCollectionFields } from '../../../../schema-initializer/buttons/FormItemInitializers'; import {
AssociatedFields,
ExtendCollectionFields,
ParentCollectionFields,
} from '../../../../schema-initializer/buttons/FormItemInitializers';
import { gridRowColWrap, useFormItemInitializerFields } from '../../../../schema-initializer/utils'; import { gridRowColWrap, useFormItemInitializerFields } from '../../../../schema-initializer/utils';
/** /**
@ -22,6 +26,10 @@ export const formItemInitializers_deprecated = new CompatibleSchemaInitializer({
name: 'parentCollectionFields', name: 'parentCollectionFields',
Component: ParentCollectionFields, Component: ParentCollectionFields,
}, },
{
name: 'extendCollectionFields',
Component: ExtendCollectionFields,
},
{ {
name: 'associationFields', name: 'associationFields',
Component: AssociatedFields, Component: AssociatedFields,
@ -55,6 +63,10 @@ export const formItemInitializers = new CompatibleSchemaInitializer(
name: 'parentCollectionFields', name: 'parentCollectionFields',
Component: ParentCollectionFields, Component: ParentCollectionFields,
}, },
{
name: 'extendCollectionFields',
Component: ExtendCollectionFields,
},
{ {
name: 'associationFields', name: 'associationFields',
Component: AssociatedFields, Component: AssociatedFields,

View File

@ -1,11 +1,13 @@
import React from 'react'; import React, { useContext } from 'react';
import { Field, observer, useField, useForm } from '@tachybase/schema'; import { Field, observer, SchemaOptionsContext, useField, useForm } from '@tachybase/schema';
import _ from 'lodash';
import { SchemaComponentOptions } from '../../'; import { SchemaComponentOptions } from '../../';
import { useAssociationCreateActionProps as useCAP } from '../../../block-provider/hooks'; import { useAssociationCreateActionProps as useCAP } from '../../../block-provider/hooks';
import { useCollection_deprecated } from '../../../collection-manager'; import { useCollection_deprecated } from '../../../collection-manager';
import { AssociationFieldProvider } from './AssociationFieldProvider'; import { AssociationFieldProvider } from './AssociationFieldProvider';
import { AssociationSelect } from './AssociationSelect'; import { AssociationSelect as Select } from './AssociationSelect';
import { CreateRecordAction } from './components/CreateRecordAction'; import { CreateRecordAction } from './components/CreateRecordAction';
import { InternalFileManager } from './FileManager'; import { InternalFileManager } from './FileManager';
import { useAssociationFieldContext } from './hooks'; import { useAssociationFieldContext } from './hooks';
@ -23,6 +25,8 @@ const EditableAssociationField = observer(
const field: Field = useField(); const field: Field = useField();
const form = useForm(); const form = useForm();
const { options: collectionField, currentMode } = useAssociationFieldContext(); const { options: collectionField, currentMode } = useAssociationFieldContext();
const { components } = useContext(SchemaOptionsContext);
const AssociationSelect = _.get(components, 'AlternativeAssociationSelect') || Select;
const useCreateActionProps = () => { const useCreateActionProps = () => {
const { onClick } = useCAP(); const { onClick } = useCAP();

View File

@ -1,10 +1,14 @@
import React from 'react'; import React from 'react';
import { useForm } from '@tachybase/schema';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaInitializerChildren } from '../../application'; import { SchemaInitializerChildren, SchemaInitializerItemType } from '../../application';
import { useCompile } from '../../schema-component'; import { useCollectionManager_deprecated } from '../../collection-manager';
import { useCollectionManager, useExtendCollections } from '../../data-source';
import { useActionContext, useCompile } from '../../schema-component';
import { import {
removeGridFormItem,
useAssociatedFormItemInitializerFields, useAssociatedFormItemInitializerFields,
useFilterAssociatedFormItemInitializerFields, useFilterAssociatedFormItemInitializerFields,
useFilterInheritsFormItemInitializerFields, useFilterInheritsFormItemInitializerFields,
@ -29,6 +33,84 @@ export const ParentCollectionFields = () => {
return <SchemaInitializerChildren>{res}</SchemaInitializerChildren>; return <SchemaInitializerChildren>{res}</SchemaInitializerChildren>;
}; };
export const ExtendCollectionFields = (props) => {
const collections = useExtendCollections();
const form = useForm();
const compile = useCompile();
const { getInterface, getCollection } = useCollectionManager_deprecated();
const { fieldSchema } = useActionContext();
if (!collections) {
return null;
}
const children = collections.map((collection) => {
// FIXME
const readPretty = form.readPretty;
const block = 'Form';
const action = fieldSchema?.['x-action'];
return {
type: 'subMenu',
title: compile(collection.title),
children: collection.fields
?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
const targetCollection = getCollection(field.target);
const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file';
const isAssociationField = targetCollection;
const fieldNames = field?.uiSchema['x-component-props']?.['fieldNames'];
const schema = {
type: 'string',
name: field.name,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${collection.name}.${field.name}`,
'x-component-props': isFileCollection
? {
fieldNames: {
label: 'preview',
value: 'id',
},
}
: isAssociationField && fieldNames
? {
fieldNames: { ...fieldNames, label: targetCollection?.titleField || fieldNames.label },
}
: {},
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
};
const resultItem = {
type: 'item',
name: field.name,
title: field?.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
block,
readPretty,
action,
targetCollection,
});
},
schema,
} as SchemaInitializerItemType;
return resultItem;
}),
};
});
const res = [];
res.push({
type: 'itemGroup',
title: '{{ t("Extend collections") }}',
children,
});
return <SchemaInitializerChildren>{res}</SchemaInitializerChildren>;
};
export const AssociatedFields = () => { export const AssociatedFields = () => {
const associationFields = useAssociatedFormItemInitializerFields({ const associationFields = useAssociatedFormItemInitializerFields({
readPretty: true, readPretty: true,

View File

@ -2,7 +2,6 @@ import React, { PropsWithChildren } from 'react';
import { SchemaComponentOptions } from '@tachybase/client'; import { SchemaComponentOptions } from '@tachybase/client';
import { import {
CollectionField,
ImageSearchInitializer, ImageSearchInitializer,
ImageSearchItemIntializer, ImageSearchItemIntializer,
ImageSearchItemToolbar, ImageSearchItemToolbar,
@ -24,8 +23,6 @@ import {
MSettings, MSettings,
MSettingsBlockInitializer, MSettingsBlockInitializer,
MTabBar, MTabBar,
NoticeBlock,
NoticeBlockInitializer,
SwiperBlock, SwiperBlock,
SwiperBlockInitializer, SwiperBlockInitializer,
SwiperPage, SwiperPage,
@ -75,22 +72,30 @@ export const MobileCore: React.FC<PropsWithChildren> = (props) => {
TabSearchCollapsibleInputMItem, TabSearchCollapsibleInputMItem,
TabSearchFieldSchemaInitializerGadget, TabSearchFieldSchemaInitializerGadget,
ImageSearchView: ImageSearchView, ImageSearchView,
ImageSearchInitializer: ImageSearchInitializer, ImageSearchInitializer,
ImageSearchProvider: ImageSearchProvider, ImageSearchProvider,
ImageSearchItemIntializer: ImageSearchItemIntializer, ImageSearchItemIntializer,
ImageSearchItemToolbar: ImageSearchItemToolbar, ImageSearchItemToolbar,
ImageSearchItemView: ImageSearchItemView, ImageSearchItemView,
// NoticeBlock,
// NoticeBlockInitializer,
MInput, MInput,
MCheckbox, MCheckbox,
MDatePicker, MDatePicker,
MRadio, MRadio,
MImageUploader, MImageUploader,
MCascader, MCascader,
CollectionField: CollectionField,
MSelect, MSelect,
// mobile alternative components
Input: MInput,
'Input.TextArea': MInput.TextArea,
'Radio.Group': MRadio.Group,
'Checkbox.Group': MCheckbox.Group,
// 只支持图片,所以暂时禁用
// 'Upload.Attachment': MImageUploader,
DatePicker: MDatePicker,
Select: MSelect,
AlternativeAssociationSelect: MSelect,
Cascader: MCascader,
}} }}
scope={{ scope={{
useGridCardBlockItemProps, useGridCardBlockItemProps,

View File

@ -1,124 +0,0 @@
import React, { useEffect, useMemo } from 'react';
import {
CollectionFieldProvider,
useCollectionField,
useCollectionManager,
useCollectionManager_deprecated,
useCompile,
useComponent,
useFormBlockContext,
useIsAllowToSetDefaultValue,
} from '@tachybase/client';
import { connect, Field, merge, useField, useFieldSchema } from '@tachybase/schema';
import { concat } from 'lodash';
import { canMobileField } from '../../CustomComponent';
import { useIsMobile } from '../../hooks';
type Props = {
component: any;
children?: React.ReactNode;
};
/**
* TODO: 初步适配
* @internal
*/
export const CollectionFieldInternalField: React.FC = (props: Props) => {
const { component } = props;
const compile = useCompile();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated();
const { uiSchema: uiSchemaOrigin, defaultValue } = useCollectionField();
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
const uiSchema = useMemo(() => compile(uiSchemaOrigin), [JSON.stringify(uiSchemaOrigin)]);
const isMobile = useIsMobile();
const currModel = getCurrModel({ getCollectionJoinField, getCollection, fieldSchema, uiSchema });
const Component = useComponent(component || uiComponent(isMobile, uiSchema, currModel) || 'Input');
const setFieldProps = (key, value) => {
field[key] = typeof field[key] === 'undefined' ? value : field[key];
};
const setRequired = () => {
if (typeof fieldSchema['required'] === 'undefined') {
field.required = !!uiSchema['required'];
}
};
const ctx = useFormBlockContext();
useEffect(() => {
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
});
// TODO: 初步适配
useEffect(() => {
if (!uiSchema) {
return;
}
setFieldProps('content', uiSchema['x-content']);
setFieldProps('title', uiSchema.title);
setFieldProps('description', uiSchema.description);
if (ctx?.form) {
const defaultVal = isAllowToSetDefaultValue() ? fieldSchema.default || defaultValue : undefined;
defaultVal !== null && defaultVal !== undefined && setFieldProps('initialValue', defaultVal);
}
if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) {
const concatSchema = concat([], uiSchema['x-validator'] || [], fieldSchema['x-validator'] || []);
field.validator = concatSchema;
}
if (fieldSchema['x-disabled'] === true) {
field.disabled = true;
}
if (fieldSchema['x-read-pretty'] === true) {
field.readPretty = true;
}
setRequired();
// @ts-ignore
field.dataSource = uiSchema.enum;
const originalProps = compile(uiSchema['x-component-props']) || {};
const componentProps = merge(originalProps, field.componentProps || {});
field.component = [Component, componentProps];
}, [JSON.stringify(uiSchema)]);
if (!uiSchema) {
return null;
}
return <Component {...props} />;
};
export const CollectionField = connect((props) => {
const fieldSchema = useFieldSchema();
return (
<CollectionFieldProvider name={fieldSchema.name}>
<CollectionFieldInternalField {...props} />
</CollectionFieldProvider>
);
});
export const uiComponent = (isMobile, uiSchema, currMode?) => {
const isMobileComponent = canMobileField(currMode || uiSchema['x-component']);
if (isMobile && isMobileComponent) {
return isMobileComponent;
} else {
return uiSchema['x-component'];
}
};
export const getCurrModel = ({ getCollectionJoinField, getCollection, fieldSchema, uiSchema }) => {
const collectionField = getCollectionJoinField(fieldSchema['x-collection-field']);
const isFileCollection = getCollection(collectionField?.target)?.template === 'file';
const isTreeCollection = getCollection(collectionField?.target)?.template === 'tree';
const currentMode =
fieldSchema['x-component-props']?.mode ||
(isFileCollection ? 'FileManager' : isTreeCollection ? 'Cascader' : 'Select');
return currentMode && uiSchema['x-component'] === 'AssociationField' ? currentMode : undefined;
};
CollectionField.displayName = 'CollectionField';

View File

@ -1,147 +0,0 @@
import {
AssociatedFields,
CompatibleSchemaInitializer,
formItemInitializers_deprecated,
gridRowColWrap,
ParentCollectionFields,
SchemaInitializerItemType,
useActionContext,
useCollection_deprecated,
useCollectionManager_deprecated,
} from '@tachybase/client';
import { Schema, useForm } from '@tachybase/schema';
import { canMobileField } from '../../CustomComponent';
import { useIsMobile } from '../../hooks';
export const useFormItemInitializerFields = (options?: any) => {
const { name, currentFields } = useCollection_deprecated();
const isMobile = useIsMobile();
const { getInterface, getCollection } = useCollectionManager_deprecated();
const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const { fieldSchema } = useActionContext();
const action = fieldSchema?.['x-action'];
return currentFields
?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
const targetCollection = getCollection(field.target);
const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file';
const isAssociationField = targetCollection;
const fieldNames = field?.uiSchema['x-component-props']?.['fieldNames'];
const isMobileComponent = canMobileField(field.uiSchema['x-component']);
const schema = {
type: 'string',
name: field.name,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': isFileCollection
? {
fieldNames: {
label: 'preview',
value: 'id',
},
}
: isAssociationField && fieldNames
? {
fieldNames: { ...fieldNames, label: targetCollection?.titleField || fieldNames.label },
}
: {},
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
};
if (isMobile && isMobileComponent) {
schema['x-component-props']['component'] = isMobileComponent;
}
const resultItem = {
type: 'item',
name: field.name,
title: field?.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
block,
readPretty,
action,
targetCollection,
});
},
schema,
} as SchemaInitializerItemType;
if (block == 'Kanban') {
resultItem['find'] = (schema: Schema, key: string, action: string) => {
const s = findSchema(schema, 'x-component', block);
return findSchema(s, key, action);
};
}
return resultItem;
});
};
export const MobileFormItemInitializers = new CompatibleSchemaInitializer(
{
name: 'form:configureFields',
wrap: gridRowColWrap,
icon: 'SettingOutlined',
title: '{{t("Configure fields")}}',
items: [
{
type: 'itemGroup',
name: 'displayFields',
title: '{{t("Display fields")}}',
useChildren: useFormItemInitializerFields,
},
{
name: 'parentCollectionFields',
Component: ParentCollectionFields,
},
{
name: 'associationFields',
Component: AssociatedFields,
},
{
name: 'divider',
type: 'divider',
},
{
name: 'addText',
title: '{{t("Add text")}}',
Component: 'MarkdownFormItemInitializer',
},
],
},
formItemInitializers_deprecated,
);
export const removeGridFormItem = (schema, cb) => {
cb(schema, {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
});
};
const findSchema = (schema: Schema, key: string, action: string) => {
if (!Schema.isSchemaInstance(schema)) return null;
return schema.reduceProperties((buf, s) => {
if (s[key] === action) {
return s;
}
if (s['x-component'] !== 'Action.Container' && s['x-component'] !== 'AssociationField.Viewer') {
const c = findSchema(s, key, action);
if (c) {
return c;
}
}
return buf;
});
};

View File

@ -1 +0,0 @@
export * from './MobileFormItemInitializers';

View File

@ -8,6 +8,4 @@ export * from './image-search';
export * from './swiper'; export * from './swiper';
export * from './tab-search'; export * from './tab-search';
export * from './notice'; export * from './notice';
export * from './form';
export * from './antd-mobile'; export * from './antd-mobile';
export * from './collection-field';

View File

@ -2,6 +2,6 @@ import React, { createContext } from 'react';
export const MobileContext = createContext<any>({ isMobile: false }); export const MobileContext = createContext<any>({ isMobile: false });
export const MobileProvider = (props) => { export const MobileProvider = ({ children }) => {
return <MobileContext.Provider value={{ isMobile: true }}> {props.children}</MobileContext.Provider>; return <MobileContext.Provider value={{ isMobile: true }}>{children}</MobileContext.Provider>;
}; };

View File

@ -9,7 +9,6 @@ import {
ImageSearchItemFieldSettings, ImageSearchItemFieldSettings,
mBlockInitializers, mBlockInitializers,
mBlockInitializers_deprecated, mBlockInitializers_deprecated,
MobileFormItemInitializers,
SwiperFieldSettings, SwiperFieldSettings,
SwiperPage, SwiperPage,
TabSearchFieldSchemaInitializer, TabSearchFieldSchemaInitializer,
@ -35,7 +34,6 @@ export class MobileClientPlugin extends Plugin {
this.app.schemaSettingsManager.add(SwiperFieldSettings); this.app.schemaSettingsManager.add(SwiperFieldSettings);
this.app.schemaSettingsManager.add(ImageSearchItemFieldSettings); this.app.schemaSettingsManager.add(ImageSearchItemFieldSettings);
this.app.schemaSettingsManager.add(TabSearchItemFieldSettings); this.app.schemaSettingsManager.add(TabSearchItemFieldSettings);
this.app.schemaInitializerManager.add(MobileFormItemInitializers);
} }
addSettings() { addSettings() {