feat: support mobile field related (#941)
Reviewed-on: daoyoucloud/tachybase#941 Reviewed-by: sealday <zhanglin@daoyoucloud.com> Co-authored-by: bai.zixv <bai.zixv@foxmail.com> Co-committed-by: bai.zixv <bai.zixv@foxmail.com>
This commit is contained in:
parent
9e63e1c392
commit
4f26e6e8c9
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RecursionField } from '@tachybase/schema';
|
import { RecursionField, useFieldSchema } from '@tachybase/schema';
|
||||||
import { useTabSearchAction } from './TabSearchAction';
|
import { useDesigner, useSchemaInitializerRender } from '@tachybase/client';
|
||||||
|
import { isTabSearchCollapsibleInputItem } from '../utils';
|
||||||
|
|
||||||
export const TabSearch = () => {
|
export const TabSearch = () => {
|
||||||
const { fieldSchema, Designer, render, filterProperties } = useTabSearchAction();
|
const { fieldSchema, Designer, render, filterProperties } = useAction();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Designer />
|
<Designer />
|
||||||
@ -12,3 +13,25 @@ export const TabSearch = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const useAction = () => {
|
||||||
|
const Designer = useDesigner();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
|
||||||
|
|
||||||
|
const filterProperties = (s) => {
|
||||||
|
if (!isTabSearchCollapsibleInputItem(s['x-component'])) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// 保留第一个,如果进入这个判断一定有值,所以取0不会出错
|
||||||
|
return (
|
||||||
|
s === Object.values(s.parent.properties).filter((s) => isTabSearchCollapsibleInputItem(s['x-component']))[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
fieldSchema,
|
||||||
|
Designer,
|
||||||
|
render,
|
||||||
|
filterProperties,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import { useDesigner, useSchemaInitializerRender } from '@tachybase/client';
|
|
||||||
import { useFieldSchema } from '@tachybase/schema';
|
|
||||||
import { isTabSearchCollapsibleInputItem } from '../utils';
|
|
||||||
|
|
||||||
export const useTabSearchAction = () => {
|
|
||||||
const Designer = useDesigner();
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
|
|
||||||
|
|
||||||
const filterProperties = (s) => {
|
|
||||||
if (!isTabSearchCollapsibleInputItem(s['x-component'])) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// 保留第一个,如果进入这个判断一定有值,所以取0不会出错
|
|
||||||
return (
|
|
||||||
s === Object.values(s.parent.properties).filter((s) => isTabSearchCollapsibleInputItem(s['x-component']))[0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
fieldSchema,
|
|
||||||
Designer,
|
|
||||||
render,
|
|
||||||
filterProperties,
|
|
||||||
};
|
|
||||||
};
|
|
@ -23,7 +23,7 @@ export const ISelect = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ textAlign: 'center', width: '100%' }}>
|
<span style={{ textAlign: 'center', width: '100%' }}>
|
||||||
{options.find((option) => option.value === customLabelKey).label}
|
{options.find((option) => option.value === customLabelKey)?.label}
|
||||||
</span>
|
</span>
|
||||||
<DownOutline />
|
<DownOutline />
|
||||||
{options.length <= 5 ? (
|
{options.length <= 5 ? (
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { SortableItem, withDynamicSchemaProps } from '@tachybase/client';
|
import { SortableItem, withDynamicSchemaProps } from '@tachybase/client';
|
||||||
import { ConfigProvider, Menu } from 'antd';
|
import { ConfigProvider, Menu } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTabSearchFieldItemAction } from './TabSearchFieldItemAction';
|
import { useAction } from './TabSearchFieldMItem';
|
||||||
|
|
||||||
export const TabSearchFieldItem = withDynamicSchemaProps(
|
export const TabSearchFieldItem = withDynamicSchemaProps(
|
||||||
(props) => {
|
(props) => {
|
||||||
const { collectionField, Designer, items, onSelect } = useTabSearchFieldItemAction(props);
|
const { collectionField, Designer, items, onSelect } = useAction(props);
|
||||||
|
|
||||||
if (!collectionField) {
|
if (!collectionField) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { MenuProps } from 'antd';
|
|
||||||
import { useCollection, useDesigner } from '@tachybase/client';
|
|
||||||
import { useFieldSchema } from '@tachybase/schema';
|
|
||||||
import { useTranslation } from '../../../../../../locale';
|
|
||||||
|
|
||||||
export const useTabSearchFieldItemAction = (props) => {
|
|
||||||
const { list, onSelected, valueKey: _valueKey, labelKey: _labelKey, filterKey } = props;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
const c = useCollection();
|
|
||||||
const collectionField = React.useMemo(
|
|
||||||
() => c?.getField(fieldSchema['fieldName'] as any),
|
|
||||||
[c, fieldSchema['fieldName']],
|
|
||||||
);
|
|
||||||
const Designer = useDesigner();
|
|
||||||
const valueKey = _valueKey || collectionField?.targetKey || 'id';
|
|
||||||
const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
|
|
||||||
const onSelect = (itemKey) => {
|
|
||||||
const key = itemKey.keyPath?.[0] || itemKey;
|
|
||||||
onSelected([key], filterKey);
|
|
||||||
};
|
|
||||||
const fieldNames = {
|
|
||||||
title: labelKey || valueKey,
|
|
||||||
key: valueKey,
|
|
||||||
};
|
|
||||||
const items: MenuProps['items'] = [];
|
|
||||||
|
|
||||||
if (list?.length) {
|
|
||||||
list.forEach((value) => {
|
|
||||||
items.push({
|
|
||||||
label: value[fieldNames.title],
|
|
||||||
key: value[fieldNames.key],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
items?.unshift({
|
|
||||||
label: t('all'),
|
|
||||||
key: 'all',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
collectionField,
|
|
||||||
Designer,
|
|
||||||
items,
|
|
||||||
onSelect,
|
|
||||||
};
|
|
||||||
};
|
|
@ -8,20 +8,25 @@ import { canBeOptionalField } from '../../utils';
|
|||||||
export const useTabSearchFieldItemProps = () => {
|
export const useTabSearchFieldItemProps = () => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
const optionalFieldList = useOptionalFieldList();
|
const currentCollectionName = fieldSchema['x-component-props']?.currentCollection || collection?.name;
|
||||||
|
const optionalFieldList = useOptionalFieldList(currentCollectionName);
|
||||||
|
|
||||||
const cm = useCollectionManager();
|
const cm = useCollectionManager();
|
||||||
const collectionField = useMemo(
|
const collectionField = useMemo(
|
||||||
() => collection?.getField(fieldSchema['fieldName'] as any),
|
() => collection?.getField(fieldSchema['fieldName'] as any),
|
||||||
[collection, fieldSchema['fieldName']],
|
[collection, fieldSchema['fieldName']],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { onSelected } = useTabSearchCollapsibleInputItem();
|
const { onSelected } = useTabSearchCollapsibleInputItem();
|
||||||
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
|
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
|
||||||
if (!collection) return;
|
if (!collection) return;
|
||||||
result.valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id';
|
result.valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id';
|
||||||
result.labelKey = fieldSchema['x-component-props']?.fieldNames?.label || result.valueKey;
|
result.labelKey = fieldSchema['x-component-props']?.fieldNames?.label || result.valueKey;
|
||||||
const fieldInterface = fieldSchema['x-component-props'].interface;
|
const fieldInterface = fieldSchema['x-component-props']?.interface;
|
||||||
if (canBeOptionalField(fieldInterface)) {
|
if (canBeOptionalField(fieldInterface)) {
|
||||||
const field = optionalFieldList.find((field) => field.name === fieldSchema['fieldName']);
|
const field = optionalFieldList.find(
|
||||||
|
(field) => field.name === fieldSchema['fieldName'].split('.').findLast(Boolean),
|
||||||
|
);
|
||||||
const operatorMap = {
|
const operatorMap = {
|
||||||
select: '$in',
|
select: '$in',
|
||||||
multipleSelect: '$anyOf',
|
multipleSelect: '$anyOf',
|
||||||
@ -29,12 +34,15 @@ export const useTabSearchFieldItemProps = () => {
|
|||||||
radioGroup: '$in',
|
radioGroup: '$in',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fieldSchema['fieldName'];
|
||||||
|
|
||||||
const _list = field?.uiSchema?.enum || [];
|
const _list = field?.uiSchema?.enum || [];
|
||||||
result.valueKey = 'value';
|
result.valueKey = 'value';
|
||||||
result.labelKey = 'label';
|
result.labelKey = 'label';
|
||||||
result.list = _list;
|
result.list = _list;
|
||||||
result.filterKey = `${field.name}.${operatorMap[field.interface]}`;
|
result.filterKey = `${fieldSchema['fieldName']}.${operatorMap[field?.interface]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
list: result.list,
|
list: result.list,
|
||||||
valueKey: result.valueKey,
|
valueKey: result.valueKey,
|
||||||
@ -44,8 +52,9 @@ export const useTabSearchFieldItemProps = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const useOptionalFieldList = () => {
|
const useOptionalFieldList = (collectionName) => {
|
||||||
const collection = useCollection();
|
const cm = useCollectionManager();
|
||||||
|
const collection = cm.getCollection(collectionName);
|
||||||
const currentFields = collection?.fields ?? [];
|
const currentFields = collection?.fields ?? [];
|
||||||
return currentFields.filter((field) => canBeOptionalField(field.interface) && field.uiSchema.enum);
|
return currentFields.filter((field) => canBeOptionalField(field.interface) && field.uiSchema.enum);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { SortableItem, withDynamicSchemaProps } from '@tachybase/client';
|
import { SortableItem, useCollection, useDesigner, withDynamicSchemaProps } from '@tachybase/client';
|
||||||
import React from 'react';
|
import { useFieldSchema } from '@tachybase/schema';
|
||||||
import { useTabSearchFieldItemAction } from './TabSearchFieldItemAction';
|
import { MenuProps } from 'antd';
|
||||||
import { Tabs } from 'antd-mobile';
|
import { Tabs } from 'antd-mobile';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from '../../../../../../locale';
|
||||||
|
|
||||||
export const TabSearchFieldMItem = withDynamicSchemaProps(
|
export const TabSearchFieldMItem = withDynamicSchemaProps(
|
||||||
(props) => {
|
(props) => {
|
||||||
const { collectionField, Designer, items, onSelect } = useTabSearchFieldItemAction(props);
|
const { collectionField, Designer, items, onSelect } = useAction(props);
|
||||||
|
|
||||||
if (!collectionField) {
|
if (!collectionField) {
|
||||||
return null;
|
return null;
|
||||||
@ -15,7 +17,7 @@ export const TabSearchFieldMItem = withDynamicSchemaProps(
|
|||||||
<SortableItem>
|
<SortableItem>
|
||||||
<Designer />
|
<Designer />
|
||||||
<Tabs style={{ '--title-font-size': '12px', color: '#a5a5a5' }} onChange={onSelect}>
|
<Tabs style={{ '--title-font-size': '12px', color: '#a5a5a5' }} onChange={onSelect}>
|
||||||
{items.map((value) => (
|
{items.map((value: any) => (
|
||||||
<Tabs.Tab title={value.label} key={value.key} />
|
<Tabs.Tab title={value.label} key={value.key} />
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@ -24,3 +26,46 @@ export const TabSearchFieldMItem = withDynamicSchemaProps(
|
|||||||
},
|
},
|
||||||
{ displayName: 'TabSearchFieldItem' },
|
{ displayName: 'TabSearchFieldItem' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const useAction = (props) => {
|
||||||
|
const { list, onSelected, valueKey: _valueKey, labelKey: _labelKey, filterKey } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const c = useCollection();
|
||||||
|
const collectionField = React.useMemo(
|
||||||
|
() => c?.getField(fieldSchema['fieldName'] as any),
|
||||||
|
[c, fieldSchema['fieldName']],
|
||||||
|
);
|
||||||
|
const Designer = useDesigner();
|
||||||
|
const valueKey = _valueKey || collectionField?.targetKey || 'id';
|
||||||
|
const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
|
||||||
|
const onSelect = (itemKey) => {
|
||||||
|
const key = itemKey.keyPath?.[0] || itemKey;
|
||||||
|
onSelected([key], filterKey);
|
||||||
|
};
|
||||||
|
const fieldNames = {
|
||||||
|
title: labelKey || valueKey,
|
||||||
|
key: valueKey,
|
||||||
|
};
|
||||||
|
const items: MenuProps['items'] = [];
|
||||||
|
|
||||||
|
if (list?.length) {
|
||||||
|
list.forEach((value) => {
|
||||||
|
items.push({
|
||||||
|
label: value[fieldNames.title],
|
||||||
|
key: value[fieldNames.key],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items?.unshift({
|
||||||
|
label: t('all'),
|
||||||
|
key: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
collectionField,
|
||||||
|
Designer,
|
||||||
|
items,
|
||||||
|
onSelect,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
// 关系字段类型
|
// 关系字段类型
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import {
|
import { useCollection, useCollectionManager, useDataSourceHeaders, useRequest } from '@tachybase/client';
|
||||||
useCollection,
|
|
||||||
useCollectionManager,
|
|
||||||
useDataSourceHeaders,
|
|
||||||
useFilterBlock,
|
|
||||||
useRequest,
|
|
||||||
} from '@tachybase/client';
|
|
||||||
import { useField, useFieldSchema } from '@tachybase/schema';
|
import { useField, useFieldSchema } from '@tachybase/schema';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useTabSearchCollapsibleInputItem } from './hooks';
|
import { useTabSearchCollapsibleInputItem } from './hooks';
|
||||||
@ -24,7 +18,10 @@ export const useTabSearchFieldItemRelatedProps = () => {
|
|||||||
() => collection?.getField(fieldSchema['fieldName'] as any),
|
() => collection?.getField(fieldSchema['fieldName'] as any),
|
||||||
[collection, fieldSchema['fieldName']],
|
[collection, fieldSchema['fieldName']],
|
||||||
);
|
);
|
||||||
const collectionFieldName = collectionField?.name;
|
const currentCollectionName = fieldSchema['x-component-props'].currentCollection;
|
||||||
|
const collectionFieldName =
|
||||||
|
currentCollectionName === collection?.name ? collectionField?.name : fieldSchema['fieldName'];
|
||||||
|
|
||||||
const fieldInterface = fieldSchema['x-component-props'].interface;
|
const fieldInterface = fieldSchema['x-component-props'].interface;
|
||||||
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
|
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
|
||||||
const { onSelected } = useTabSearchCollapsibleInputItem();
|
const { onSelected } = useTabSearchCollapsibleInputItem();
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from './TabSearch';
|
export * from './TabSearch';
|
||||||
export * from './TabSearchAction';
|
|
||||||
export * from './field-item';
|
export * from './field-item';
|
||||||
|
@ -0,0 +1,371 @@
|
|||||||
|
import {
|
||||||
|
CollectionFieldOptions_deprecated,
|
||||||
|
CollectionManager,
|
||||||
|
FieldOptions,
|
||||||
|
InheritanceCollectionMixin,
|
||||||
|
SchemaInitializerChildren,
|
||||||
|
SchemaInitializerItemType,
|
||||||
|
isAssocField,
|
||||||
|
useCollection,
|
||||||
|
useCollectionManager,
|
||||||
|
useDataSource,
|
||||||
|
useDataSourceManager,
|
||||||
|
} from '@tachybase/client';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { tval } from '../../../../../locale';
|
||||||
|
import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils';
|
||||||
|
import { useIsMobile } from '../components/field-item/hooks';
|
||||||
|
|
||||||
|
interface ItemInterface {
|
||||||
|
field: FieldOptions;
|
||||||
|
label: string;
|
||||||
|
schemaName: string;
|
||||||
|
collectionName: string;
|
||||||
|
getCollectionFields;
|
||||||
|
processedCollections: string[];
|
||||||
|
collectionManager: CollectionManager;
|
||||||
|
currentCollection?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabSearchAssociatedFields = () => {
|
||||||
|
// NOTE: 这里的 useAssociationFields 借鉴的是 useFilterAssociatedFormItemInitializerFields
|
||||||
|
const associationFieldsText = useAssociationFields('textField');
|
||||||
|
const associationFieldsChoice = useAssociationFields('choiceField');
|
||||||
|
// 这里的 children, 后续实际不是用来渲染的, 是个普通的 props 字段
|
||||||
|
// XXX: refactor, 不好的设计, 用 children 来做普通 props 的数据传递
|
||||||
|
const children: any[] = [
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: tval('Display association Textfields', false),
|
||||||
|
children: associationFieldsText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: tval('Display association Choicefields', false),
|
||||||
|
children: associationFieldsChoice,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <SchemaInitializerChildren>{children}</SchemaInitializerChildren>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useAssociationFields(type: 'textField' | 'choiceField') {
|
||||||
|
const collection = useCollection<{ allFields: FieldOptions[] }>();
|
||||||
|
|
||||||
|
const { name, allFields } = collection || {};
|
||||||
|
|
||||||
|
const { getCollectionFields } = useCollectionFields_custom();
|
||||||
|
const isMobield = useIsMobile();
|
||||||
|
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
|
||||||
|
const filteredAssociationFields = allFields?.filter(({ interface: _interface }) => {
|
||||||
|
return canBeRelatedField(_interface);
|
||||||
|
});
|
||||||
|
|
||||||
|
const paramsItem = {
|
||||||
|
collectionName: name,
|
||||||
|
getCollectionFields,
|
||||||
|
processedCollections: [],
|
||||||
|
collectionManager: cm,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mappedAssociationFields = [];
|
||||||
|
if (type === 'textField') {
|
||||||
|
mappedAssociationFields = filteredAssociationFields.map((field) => {
|
||||||
|
// 将关联表字段转换为 schema 树
|
||||||
|
const label = canBeRelatedField(field.interface)
|
||||||
|
? cm.getCollection(`${name}.${field.name}`)?.titleField
|
||||||
|
: field.name;
|
||||||
|
|
||||||
|
const result = getItemInput(
|
||||||
|
{
|
||||||
|
...paramsItem,
|
||||||
|
field,
|
||||||
|
label,
|
||||||
|
schemaName: field.name,
|
||||||
|
},
|
||||||
|
isMobield,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
} else if (type === 'choiceField') {
|
||||||
|
mappedAssociationFields = filteredAssociationFields.map((field) => {
|
||||||
|
// 将关联表字段转换为 schema 树
|
||||||
|
const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id';
|
||||||
|
const result = getItemChoice(
|
||||||
|
{
|
||||||
|
...paramsItem,
|
||||||
|
field,
|
||||||
|
label,
|
||||||
|
schemaName: field.name,
|
||||||
|
},
|
||||||
|
isMobield,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const associationFields = mappedAssociationFields.filter(Boolean);
|
||||||
|
return associationFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: 递归生成 schema 树
|
||||||
|
function getItemInput(params: ItemInterface, isMobield?: boolean) {
|
||||||
|
const {
|
||||||
|
field,
|
||||||
|
label,
|
||||||
|
schemaName,
|
||||||
|
collectionName,
|
||||||
|
getCollectionFields,
|
||||||
|
processedCollections,
|
||||||
|
collectionManager: cm,
|
||||||
|
} = params;
|
||||||
|
const { interface: _interface } = field;
|
||||||
|
const isCanBeOptional = canBeOptionalField(_interface);
|
||||||
|
const isCanBeRelated = canBeRelatedField(_interface);
|
||||||
|
|
||||||
|
// NOTE: 目前只支持 m2o 和 obo 关联
|
||||||
|
if (['m2o', 'obo'].includes(_interface)) {
|
||||||
|
if (processedCollections.includes(field.target)) return null;
|
||||||
|
const subFields = getCollectionFields(field.target);
|
||||||
|
return {
|
||||||
|
type: 'subMenu',
|
||||||
|
name: field.uiSchema?.title,
|
||||||
|
title: field.uiSchema?.title,
|
||||||
|
// 将所有的关联字段排在最后
|
||||||
|
children: subFields
|
||||||
|
.toSorted((a, b) => +Object.hasOwn(a, 'target') - +Object.hasOwn(b, 'target'))
|
||||||
|
.map((subField) => {
|
||||||
|
const label = canBeRelatedField(field.interface)
|
||||||
|
? cm.getCollection(`${name}.${field.name}`)?.titleField
|
||||||
|
: field.name;
|
||||||
|
return getItemInput({
|
||||||
|
field: subField,
|
||||||
|
label,
|
||||||
|
schemaName: `${schemaName}.${subField.name}`,
|
||||||
|
collectionName: collectionName,
|
||||||
|
getCollectionFields: getCollectionFields,
|
||||||
|
processedCollections: [...processedCollections, field.target],
|
||||||
|
collectionManager: cm,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.filter(Boolean),
|
||||||
|
} as SchemaInitializerItemType;
|
||||||
|
}
|
||||||
|
// 然后排除其他类型的关联字段
|
||||||
|
if (isAssocField(field)) return null;
|
||||||
|
|
||||||
|
if (!(canBeSearchField(_interface) && !canBeRelatedField(_interface) && !canBeDataField(_interface))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 制造 schema
|
||||||
|
// const schema = ;
|
||||||
|
|
||||||
|
// {
|
||||||
|
// type: 'itemGroup',
|
||||||
|
// name: 'textFields',
|
||||||
|
// title: tval('Text fields'),
|
||||||
|
// useChildren: useChildrenTextField,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'itemGroup',
|
||||||
|
// name: 'choicesFields',
|
||||||
|
// title: tval('Choices fields'),
|
||||||
|
// useChildren: useChildrenChoiceField,
|
||||||
|
// },
|
||||||
|
|
||||||
|
return {
|
||||||
|
// type: 'item',
|
||||||
|
name: field.uiSchema?.title || field.name,
|
||||||
|
title: field.uiSchema?.title || field.name,
|
||||||
|
Component: 'TabSearchFieldSchemaInitializerGadget',
|
||||||
|
// remove: removeGridFormItem,
|
||||||
|
schema: {
|
||||||
|
// type: 'string',
|
||||||
|
type: 'void',
|
||||||
|
name: schemaName,
|
||||||
|
title: `${field.uiSchema?.title}`,
|
||||||
|
fieldName: `${schemaName}`,
|
||||||
|
// 'x-designer': 'FormItem.FilterFormDesigner',
|
||||||
|
// 'x-designer-props': {
|
||||||
|
// // 在 useOperatorList 中使用,用于获取对应的操作符列表
|
||||||
|
// interface: field.interface,
|
||||||
|
// },
|
||||||
|
'x-toolbar': 'CollapseItemSchemaToolbar',
|
||||||
|
'x-settings': 'fieldSettings:TabSearchItem',
|
||||||
|
'x-collection-field': `${collectionName}.${schemaName}`,
|
||||||
|
// 'x-decorator': 'FormItem',
|
||||||
|
// 'x-component': 'CollectionField',
|
||||||
|
'x-component': matchTruthValue({
|
||||||
|
['TabSearchCollapsibleInputMItem']: isMobield,
|
||||||
|
['TabSearchCollapsibleInputItem']: !isMobield,
|
||||||
|
}),
|
||||||
|
'x-component-props': {
|
||||||
|
fieldNames: {
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
interface: field.interface,
|
||||||
|
collectionName: collectionName,
|
||||||
|
correlation: field.name,
|
||||||
|
},
|
||||||
|
'x-use-component-props': matchTruthValue({
|
||||||
|
['useTabSearchFieldItemProps']: isCanBeOptional,
|
||||||
|
['useTabSearchFieldItemRelatedProps']: isCanBeRelated,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 'x-read-pretty': false,
|
||||||
|
},
|
||||||
|
} as SchemaInitializerItemType;
|
||||||
|
}
|
||||||
|
// NOTE: 递归生成 schema 树
|
||||||
|
function getItemChoice(params: ItemInterface, isMobield?: boolean) {
|
||||||
|
const {
|
||||||
|
field,
|
||||||
|
label,
|
||||||
|
schemaName,
|
||||||
|
collectionName,
|
||||||
|
getCollectionFields,
|
||||||
|
processedCollections,
|
||||||
|
collectionManager: cm,
|
||||||
|
currentCollection,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const { interface: _interface } = field;
|
||||||
|
const isCanBeOptional = canBeOptionalField(_interface);
|
||||||
|
const isCanBeRelated = canBeRelatedField(_interface);
|
||||||
|
|
||||||
|
// NOTE: 目前只支持 m2o 和 obo 关联
|
||||||
|
if (['m2o', 'obo'].includes(_interface) && !field.choiceTag) {
|
||||||
|
if (processedCollections.includes(field.target)) return null;
|
||||||
|
const subFields = getCollectionFields(field.target);
|
||||||
|
return {
|
||||||
|
type: 'subMenu',
|
||||||
|
name: field.uiSchema?.title,
|
||||||
|
title: field.uiSchema?.title,
|
||||||
|
// 将所有的关联字段排在最后
|
||||||
|
children: subFields
|
||||||
|
.reduce((acc, subField) => {
|
||||||
|
if (Object.hasOwn(subField, 'target') && ['m2o', 'obo'].includes(subField.interface)) {
|
||||||
|
// NOTE: 将当前的关联字段复制一份, 作为当前层级的选择展示,并打上选择标记 choiceTag
|
||||||
|
acc.push({
|
||||||
|
...subField,
|
||||||
|
choiceTag: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [...acc, subField];
|
||||||
|
}, [])
|
||||||
|
// NOTE: 排序规则: 1. 关联字段放最后 2. 选择复制字段次之 3. 其他字段置前
|
||||||
|
.toSorted(
|
||||||
|
(a, b) =>
|
||||||
|
+Object.hasOwn(a, 'target') * 2 -
|
||||||
|
+Object.hasOwn(a, 'choiceTag') -
|
||||||
|
+Object.hasOwn(b, 'target') * 2 -
|
||||||
|
+Object.hasOwn(b, 'choiceTag'),
|
||||||
|
)
|
||||||
|
.map((subField) => {
|
||||||
|
const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id';
|
||||||
|
return getItemChoice({
|
||||||
|
field: subField,
|
||||||
|
label,
|
||||||
|
schemaName: `${schemaName}.${subField.name}`,
|
||||||
|
collectionName: collectionName,
|
||||||
|
getCollectionFields: getCollectionFields,
|
||||||
|
processedCollections: [...processedCollections, field.target],
|
||||||
|
collectionManager: cm,
|
||||||
|
currentCollection: field.target,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.filter(Boolean),
|
||||||
|
} as SchemaInitializerItemType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后排除其他类型的关联字段, 并且不要删除打上选择标记 choiceTag 的关联字段
|
||||||
|
if (isAssocField(field) && !field.choiceTag) return null;
|
||||||
|
|
||||||
|
if (!canBeOptionalField(_interface) && !canBeRelatedField(_interface)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'item',
|
||||||
|
name: field.uiSchema?.title || field.name,
|
||||||
|
title: field.uiSchema?.title || field.name,
|
||||||
|
Component: 'TabSearchFieldSchemaInitializerGadget',
|
||||||
|
// remove: removeGridFormItem,
|
||||||
|
schema: {
|
||||||
|
// type: 'string',
|
||||||
|
type: 'void',
|
||||||
|
name: schemaName,
|
||||||
|
title: `${field.uiSchema?.title}`,
|
||||||
|
fieldName: `${schemaName}`,
|
||||||
|
// 'x-designer': 'FormItem.FilterFormDesigner',
|
||||||
|
// 'x-designer-props': {
|
||||||
|
// // 在 useOperatorList 中使用,用于获取对应的操作符列表
|
||||||
|
// interface: field.interface,
|
||||||
|
// },
|
||||||
|
'x-toolbar': 'CollapseItemSchemaToolbar',
|
||||||
|
'x-settings': 'fieldSettings:TabSearchItem',
|
||||||
|
// 'x-read-pretty': false,
|
||||||
|
'x-collection-field': `${collectionName}.${schemaName}`,
|
||||||
|
// 'x-decorator': 'FormItem',
|
||||||
|
// 'x-component': 'CollectionField',
|
||||||
|
'x-component': matchTruthValue({
|
||||||
|
['TabSearchFieldMItem']: isMobield,
|
||||||
|
['TabSearchFieldItem']: !isMobield,
|
||||||
|
}),
|
||||||
|
'x-component-props': {
|
||||||
|
fieldNames: {
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
interface: field.interface,
|
||||||
|
collectionName: collectionName,
|
||||||
|
correlation: field.name,
|
||||||
|
currentCollection,
|
||||||
|
},
|
||||||
|
'x-use-component-props': matchTruthValue({
|
||||||
|
['useTabSearchFieldItemProps']: isCanBeOptional,
|
||||||
|
['useTabSearchFieldItemRelatedProps']: isCanBeRelated,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
} as SchemaInitializerItemType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: 为了不使用标记废弃的 api, 模拟复制原方法实现; 参考 useCollectionManager_deprecated
|
||||||
|
function useCollectionFields_custom(dataSourceName?: string) {
|
||||||
|
const dm = useDataSourceManager();
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const dataSourceNameValue = dataSourceName || dataSource?.key || undefined;
|
||||||
|
|
||||||
|
const getCm = useCallback(
|
||||||
|
(dataSource?: string) => {
|
||||||
|
if (cm && !dataSource) {
|
||||||
|
return cm;
|
||||||
|
} else {
|
||||||
|
return dm?.getDataSource(dataSource || dataSourceNameValue)?.collectionManager;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[cm, dm, dataSourceNameValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCollectionFields = useCallback(
|
||||||
|
(name: any, customDataSource?: string): CollectionFieldOptions_deprecated[] => {
|
||||||
|
if (!name) return [];
|
||||||
|
const collection = getCm(customDataSource)?.getCollection<InheritanceCollectionMixin>(name);
|
||||||
|
return collection?.getAllFields?.() || collection?.getFields() || [];
|
||||||
|
},
|
||||||
|
[dm, getCm],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCollectionFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchTruthValue(obj: Record<string, boolean>): string {
|
||||||
|
const result = Object.entries(obj).find(([, value]) => value);
|
||||||
|
const [key] = result || [];
|
||||||
|
return key;
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
|
import { DataBlockInitializer, useSchemaInitializer, useSchemaInitializerItem } from '@tachybase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
|
||||||
import { DataBlockInitializer, Icon, useSchemaInitializer, useSchemaInitializerItem } from '@tachybase/client';
|
|
||||||
import { createTabSearchBlockSchema } from '../create/createTabSearchBlockSchema';
|
import { createTabSearchBlockSchema } from '../create/createTabSearchBlockSchema';
|
||||||
|
|
||||||
export const TabSearchBlockInitializer = (props) => {
|
export const TabSearchBlockInitializer = (props) => {
|
||||||
|
@ -1,78 +1,100 @@
|
|||||||
import { SchemaInitializer, SchemaInitializerItemType, useCollection, useCollectionManager } from '@tachybase/client';
|
import { SchemaInitializer, useCollection, useCollectionManager } from '@tachybase/client';
|
||||||
import _ from 'lodash';
|
|
||||||
import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils';
|
|
||||||
import { createTabSearchItemSchema } from '../create/createTabSearchItemSchema';
|
|
||||||
import { useIsMobile } from '../components/field-item/hooks';
|
|
||||||
import { tval } from '../../../../../locale';
|
import { tval } from '../../../../../locale';
|
||||||
|
import { useIsMobile } from '../components/field-item/hooks';
|
||||||
const textFieldsItem: SchemaInitializerItemType<any> = {
|
import { createTabSearchItemSchema } from '../create/createTabSearchItemSchema';
|
||||||
name: 'textFields',
|
import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils';
|
||||||
type: 'itemGroup',
|
import { TabSearchAssociatedFields } from './TabSearchAssociated.fields';
|
||||||
title: tval('Text fields'),
|
|
||||||
useChildren() {
|
|
||||||
const collection = useCollection();
|
|
||||||
const associatedFields = collection.fields;
|
|
||||||
const cm = useCollectionManager();
|
|
||||||
const isMobield = useIsMobile();
|
|
||||||
const itemComponent = isMobield ? 'TabSearchCollapsibleInputMItem' : 'TabSearchCollapsibleInputItem';
|
|
||||||
const children = associatedFields
|
|
||||||
.map((field) => {
|
|
||||||
if (
|
|
||||||
!field['isForeignKey'] &&
|
|
||||||
(canBeSearchField(field.interface) || canBeRelatedField(field.interface) || canBeDataField(field.interface))
|
|
||||||
) {
|
|
||||||
const label = canBeRelatedField(field.interface)
|
|
||||||
? cm.getCollection(`${collection.name}.${field.name}`).titleField
|
|
||||||
: field.name;
|
|
||||||
return createTabSearchItemSchema({ field, itemComponent, label, collection, type: 'text' });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
return children;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const choicesFieldsItem: SchemaInitializerItemType<any> = {
|
|
||||||
name: 'choicesFields',
|
|
||||||
type: 'itemGroup',
|
|
||||||
title: tval('Choices fields'),
|
|
||||||
useChildren() {
|
|
||||||
const collection = useCollection();
|
|
||||||
const optionalList = collection.fields;
|
|
||||||
const cm = useCollectionManager();
|
|
||||||
const isMobield = useIsMobile();
|
|
||||||
const itemComponent = isMobield ? 'TabSearchFieldMItem' : 'TabSearchFieldItem';
|
|
||||||
const optionalChildren = optionalList
|
|
||||||
.map((field) => {
|
|
||||||
const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id';
|
|
||||||
if (canBeOptionalField(field.interface)) {
|
|
||||||
return createTabSearchItemSchema({
|
|
||||||
field,
|
|
||||||
itemComponent,
|
|
||||||
label,
|
|
||||||
itemUseProps: 'useTabSearchFieldItemProps',
|
|
||||||
type: 'choices',
|
|
||||||
});
|
|
||||||
} else if (canBeRelatedField(field.interface)) {
|
|
||||||
return createTabSearchItemSchema({
|
|
||||||
field,
|
|
||||||
itemComponent,
|
|
||||||
label,
|
|
||||||
itemUseProps: 'useTabSearchFieldItemRelatedProps',
|
|
||||||
type: 'choices',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
return optionalChildren;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TabSearchFieldSchemaInitializer = new SchemaInitializer({
|
export const TabSearchFieldSchemaInitializer = new SchemaInitializer({
|
||||||
name: 'tabSearch:configureFields',
|
name: 'tabSearch:configureFields',
|
||||||
style: { marginTop: 16 },
|
style: { marginTop: 16 },
|
||||||
icon: 'SettingOutlined',
|
icon: 'SettingOutlined',
|
||||||
title: tval('Configure fields'),
|
title: tval('Configure fields'),
|
||||||
items: [textFieldsItem, choicesFieldsItem],
|
items: [
|
||||||
|
{
|
||||||
|
name: 'textFields',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: tval('Text fields'),
|
||||||
|
useChildren: useChildrenTextField,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'choicesFields',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: tval('Choices fields'),
|
||||||
|
useChildren: useChildrenChoiceField,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'divider',
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'associationFields',
|
||||||
|
Component: TabSearchAssociatedFields,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'divider',
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: tval('Add text', false),
|
||||||
|
Component: 'MarkdownFormItemInitializer',
|
||||||
|
name: 'addText',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 文本字段
|
||||||
|
function useChildrenTextField() {
|
||||||
|
const collection = useCollection();
|
||||||
|
const associatedFields = collection.fields;
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const isMobield = useIsMobile();
|
||||||
|
const itemComponent = isMobield ? 'TabSearchCollapsibleInputMItem' : 'TabSearchCollapsibleInputItem';
|
||||||
|
const children = associatedFields
|
||||||
|
.map((field) => {
|
||||||
|
if (
|
||||||
|
!field['isForeignKey'] &&
|
||||||
|
(canBeSearchField(field.interface) || canBeRelatedField(field.interface) || canBeDataField(field.interface))
|
||||||
|
) {
|
||||||
|
const label = canBeRelatedField(field.interface)
|
||||||
|
? cm.getCollection(`${collection.name}.${field.name}`).titleField
|
||||||
|
: field.name;
|
||||||
|
return createTabSearchItemSchema({ field, itemComponent, label, collection, type: 'text' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择字段
|
||||||
|
function useChildrenChoiceField() {
|
||||||
|
const collection = useCollection();
|
||||||
|
const optionalList = collection.fields;
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const isMobield = useIsMobile();
|
||||||
|
const itemComponent = isMobield ? 'TabSearchFieldMItem' : 'TabSearchFieldItem';
|
||||||
|
const optionalChildren = optionalList
|
||||||
|
.map((field) => {
|
||||||
|
const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id';
|
||||||
|
if (canBeOptionalField(field.interface)) {
|
||||||
|
return createTabSearchItemSchema({
|
||||||
|
field,
|
||||||
|
itemComponent,
|
||||||
|
label,
|
||||||
|
itemUseProps: 'useTabSearchFieldItemProps',
|
||||||
|
type: 'choices',
|
||||||
|
});
|
||||||
|
} else if (canBeRelatedField(field.interface)) {
|
||||||
|
return createTabSearchItemSchema({
|
||||||
|
field,
|
||||||
|
itemComponent,
|
||||||
|
label,
|
||||||
|
itemUseProps: 'useTabSearchFieldItemRelatedProps',
|
||||||
|
type: 'choices',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return optionalChildren;
|
||||||
|
}
|
||||||
|
@ -71,11 +71,14 @@ export const TabSearchItemFieldSettings = new SchemaSettings({
|
|||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const cm = useCollectionManager();
|
const cm = useCollectionManager();
|
||||||
const c = useCollection();
|
const collection = useCollection();
|
||||||
const fieldCollection = fieldSchema['x-component-props']?.['collectionName'];
|
|
||||||
const correlation = fieldSchema['x-component-props']?.['correlation'];
|
const propsInSchema = fieldSchema['x-component-props'] || {};
|
||||||
|
const { collectionName, currentCollection, correlation } = propsInSchema;
|
||||||
|
const fieldCollection = currentCollection || collectionName || collection.name;
|
||||||
|
|
||||||
const collectionField =
|
const collectionField =
|
||||||
c.getField(fieldSchema['fieldName']) ||
|
collection.getField(fieldSchema['fieldName']) ||
|
||||||
cm.getCollectionField(fieldSchema['x-collection-field']) ||
|
cm.getCollectionField(fieldSchema['x-collection-field']) ||
|
||||||
cm.getCollection(fieldCollection + '.' + correlation);
|
cm.getCollection(fieldCollection + '.' + correlation);
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
@ -11,7 +11,14 @@ export function generateNTemplate(key: string) {
|
|||||||
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
|
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });
|
export const tval = (key: string, haveNamespace: boolean = true) => {
|
||||||
|
if (haveNamespace) {
|
||||||
|
return nTval(key, { ns: NAMESPACE })
|
||||||
|
} else {
|
||||||
|
return nTval(key)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export function useTranslation() {
|
export function useTranslation() {
|
||||||
return useT([NAMESPACE, '@tachybase/plugin-mobile-client', 'client'], {
|
return useT([NAMESPACE, '@tachybase/plugin-mobile-client', 'client'], {
|
||||||
|
@ -29,8 +29,10 @@
|
|||||||
"Mobile client access address": "Mobile client access address",
|
"Mobile client access address": "Mobile client access address",
|
||||||
"The full address is": "The full address is",
|
"The full address is": "The full address is",
|
||||||
"Preview": "Preview",
|
"Preview": "Preview",
|
||||||
"Choices fields":"Choices fields",
|
"Choices fields":"Choices fields",
|
||||||
"Text fields":"Text fields",
|
"Text fields":"Text fields",
|
||||||
|
"Display association Textfields": "Display association Textfields",
|
||||||
|
"Display association Choicefields": "Display association Choicefields",
|
||||||
"Generic properties":"Generic properties",
|
"Generic properties":"Generic properties",
|
||||||
"Search":"Search",
|
"Search":"Search",
|
||||||
"Please enter search content":"Please enter search content",
|
"Please enter search content":"Please enter search content",
|
||||||
|
@ -29,8 +29,10 @@
|
|||||||
"Mobile client access address": "移动端访问地址",
|
"Mobile client access address": "移动端访问地址",
|
||||||
"The full address is": "完整的地址是",
|
"The full address is": "完整的地址是",
|
||||||
"Preview": "预览",
|
"Preview": "预览",
|
||||||
"Choices fields":"选择字段",
|
"Choices fields":"选择字段",
|
||||||
"Text fields":"文本字段",
|
"Text fields":"文本字段",
|
||||||
|
"Display association Textfields": "文本关联字段",
|
||||||
|
"Display association Choicefields": "选择关联字段",
|
||||||
"Generic properties":"通用属性",
|
"Generic properties":"通用属性",
|
||||||
"Search":"搜索",
|
"Search":"搜索",
|
||||||
"Please enter search content":"请输入查询内容",
|
"Please enter search content":"请输入查询内容",
|
||||||
|
Loading…
Reference in New Issue
Block a user