refactor(association field): association field support tag field mode (#2251)

* refactor: association field support tag field mode

* refactor: association field support tag field mode

* refactor: locale improve

* refactor: color field

* refactor: locale improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: colorPicker refactor

* refactor: colorPicker refactor

* fix: color field failed to config  defaultValue

* style: style improve

* refactor: code improve

* fix: merge bug

* refactor: color field defaultValue

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
katherinehhh 2023-07-31 16:23:04 +08:00 committed by GitHub
parent 8f1d0d80af
commit 18bf6e507e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 613 additions and 16 deletions

View File

@ -0,0 +1,26 @@
import { defaultProps, operators } from './properties';
import { IField } from './types';
export const color: IField = {
name: 'color',
type: 'object',
group: 'basic',
order: 10,
title: '{{t("Color")}}',
default: {
type: 'string',
uiSchema: {
type: 'string',
'x-component': 'ColorPicker',
default: '#1677FF',
},
},
availableTypes: ['string'],
hasDefaultValue: true,
properties: {
...defaultProps,
},
filterable: {
operators: operators.string,
},
};

View File

@ -2,6 +2,7 @@ export * from './checkbox';
export * from './checkboxGroup'; export * from './checkboxGroup';
export * from './chinaRegion'; export * from './chinaRegion';
export * from './collection'; export * from './collection';
export * from './color';
export * from './createdAt'; export * from './createdAt';
export * from './createdBy'; export * from './createdBy';
export * from './datetime'; export * from './datetime';

View File

@ -711,6 +711,8 @@ export default {
"Date display format":"Date display format", "Date display format":"Date display format",
"Assign data scope for the template":"Assign data scope for the template", "Assign data scope for the template":"Assign data scope for the template",
"Table selected records":"Table selected records", "Table selected records":"Table selected records",
"Tag":"Tag",
"Tag color field":"Tag color field",
"Sync successfully":"Sync successfully", "Sync successfully":"Sync successfully",
"Sync from form fields":"Sync from form fields", "Sync from form fields":"Sync from form fields",
"Select all":"Select all" "Select all":"Select all"

View File

@ -621,6 +621,8 @@ export default {
"Allow add new, update and delete actions":"削除変更操作の許可", "Allow add new, update and delete actions":"削除変更操作の許可",
"Date display format":"日付表示形式", "Date display format":"日付表示形式",
"Assign data scope for the template":"テンプレートのデータ範囲の指定", "Assign data scope for the template":"テンプレートのデータ範囲の指定",
"Tag":"タブ",
"Tag color field":"ラベルの色フィールド",
"Sync successfully":"同期成功", "Sync successfully":"同期成功",
"Sync from form fields":"フォームフィールドの同期", "Sync from form fields":"フォームフィールドの同期",
"Select all":"すべて選択" "Select all":"すべて選択"

View File

@ -796,6 +796,8 @@ export default {
"Date display format":"日期显示格式", "Date display format":"日期显示格式",
"Assign data scope for the template":"为模板指定数据范围", "Assign data scope for the template":"为模板指定数据范围",
"Table selected records":"表格中选中的记录", "Table selected records":"表格中选中的记录",
"Tag":"标签",
"Tag color field":"标签颜色字段",
"Sync successfully":"同步成功", "Sync successfully":"同步成功",
"Sync from form fields":"同步表单字段", "Sync from form fields":"同步表单字段",
"Select all":"全选" "Select all":"全选"

View File

@ -0,0 +1,123 @@
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared';
import flat from 'flat';
import React, { Fragment, useRef, useState } from 'react';
import { useDesignable } from '../../';
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
import { CollectionProvider } from '../../../collection-manager';
import { RecordProvider, useRecord } from '../../../record-provider';
import { FormProvider } from '../../core';
import { useCompile } from '../../hooks';
import { ActionContextProvider, useActionContext } from '../action';
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
import schema from './schema';
import { getTabFormatValue, useLabelUiSchema } from './util';
interface IEllipsisWithTooltipRef {
setPopoverVisible: (boolean) => void;
}
const toValue = (value, placeholder) => {
if (value === null || value === undefined) {
return placeholder;
}
return value;
};
export const ReadPrettyInternalTag: React.FC = observer(
(props: any) => {
const fieldSchema = useFieldSchema();
const recordCtx = useRecord();
const { enableLink, tagColorField } = fieldSchema['x-component-props'];
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
const field = useField();
const fieldNames = useFieldNames(props);
const [visible, setVisible] = useState(false);
const insertViewer = useInsertSchema('Viewer');
const { options: collectionField } = useAssociationFieldContext();
const [record, setRecord] = useState({});
const compile = useCompile();
const { designable } = useDesignable();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const { snapshot } = useActionContext();
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
const tagColor = flat(recordCtx)[`${fieldSchema.name}.${tagColorField}`];
const renderRecords = () =>
toArr(props.value).map((record, index, arr) => {
const val = toValue(compile(record?.[fieldNames?.label || 'label']), 'N/A');
const text = getTabFormatValue(compile(labelUiSchema), val, tagColor);
return (
<Fragment key={`${record.id}_${index}`}>
<span>
{snapshot ? (
text
) : enableLink !== false ? (
<a
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
if (designable) {
insertViewer(schema.Viewer);
}
setVisible(true);
setRecord(record);
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
}}
>
{text}
</a>
) : (
text
)}
</span>
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
</Fragment>
);
});
const renderWithoutTableFieldResourceProvider = () => (
<WithoutTableFieldResource.Provider value={true}>
<FormProvider>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
basePath={field.address}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.Viewer';
}}
/>
</FormProvider>
</WithoutTableFieldResource.Provider>
);
const renderRecordProvider = () => {
const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
return collectionFieldNames && collectionFieldNames.length > 2 ? (
<RecordProvider record={recordCtx[collectionFieldNames[1]]}>
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
</RecordProvider>
) : (
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
);
};
return (
<div>
<BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
<CollectionProvider name={collectionField?.target ?? collectionField?.targetCollection}>
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
{renderRecords()}
</EllipsisWithTooltip>
<ActionContextProvider
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField?.interface === 'snapshot' }}
>
{renderRecordProvider()}
</ActionContextProvider>
</CollectionProvider>
</BlockAssociationContext.Provider>
</div>
);
},
{ displayName: 'ReadPrettyInternalTag' },
);

View File

@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react'; import { observer } from '@formily/react';
import { useField, observer } from '@formily/react'; import React from 'react';
import { AssociationFieldProvider } from './AssociationFieldProvider'; import { AssociationFieldProvider } from './AssociationFieldProvider';
import { InternalNester } from './InternalNester';
import { ReadPrettyInternalViewer } from './InternalViewer';
import { InternalSubTable } from './InternalSubTable';
import { FileManageReadPretty } from './FileManager'; import { FileManageReadPretty } from './FileManager';
import { useAssociationFieldContext } from './hooks'; import { useAssociationFieldContext } from './hooks';
import { InternalNester } from './InternalNester';
import { InternalSubTable } from './InternalSubTable';
import { ReadPrettyInternalTag } from './InternalTag';
import { ReadPrettyInternalViewer } from './InternalViewer';
const ReadPrettyAssociationField = observer( const ReadPrettyAssociationField = observer(
(props: any) => { (props: any) => {
@ -14,6 +15,7 @@ const ReadPrettyAssociationField = observer(
return ( return (
<> <>
{['Select', 'Picker'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />} {['Select', 'Picker'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />}
{currentMode === 'Tag' && <ReadPrettyInternalTag {...props} />}
{currentMode === 'Nester' && <InternalNester {...props} />} {currentMode === 'Nester' && <InternalNester {...props} />}
{currentMode === 'SubTable' && <InternalSubTable {...props} />} {currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <FileManageReadPretty {...props} />} {currentMode === 'FileManager' && <FileManageReadPretty {...props} />}

View File

@ -48,6 +48,28 @@ export const getLabelFormatValue = (labelUiSchema: ISchema, value: any, isTag =
} }
}; };
export const getTabFormatValue = (labelUiSchema: ISchema, value: any, tagColor): any => {
const options = labelUiSchema?.enum;
if (Array.isArray(options) && value) {
const values = toArr(value).map((val) => {
const opt: any = options.find((option: any) => option.value === val);
return React.createElement(Tag, { color: tagColor||opt?.color }, opt?.label);
});
return values;
}
switch (labelUiSchema?.['x-component']) {
case 'DatePicker':
return React.createElement(
Tag,
{ color: tagColor },
getDatePickerLabels({ ...labelUiSchema?.['x-component-props'], value }),
);
default:
return React.createElement(Tag, { color: tagColor }, value);
}
};
export function flatData(data) { export function flatData(data) {
const newArr = []; const newArr = [];
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {

View File

@ -0,0 +1,68 @@
import { css } from '@emotion/css';
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { ColorPicker as AntdColorPicker } from 'antd';
import cls from 'classnames';
import React from 'react';
export const ColorPicker = connect(
(props) => {
const { value, onChange, ...others } = props;
return (
<AntdColorPicker
value={value}
trigger="hover"
{...others}
destroyTooltipOnHide
getPopupContainer={(current) => current}
presets={[
{
label: 'Recommended',
colors: [
'#8BBB11',
'#52C41A',
'#13A8A8',
'#1677FF',
'#F5222D',
'#FADB14',
'#FA8C164D',
'#FADB144D',
'#52C41A4D',
'#1677FF4D',
'#2F54EB4D',
'#722ED14D',
'#EB2F964D',
],
},
]}
onChange={(color) => onChange(color.toHexString())}
/>
);
},
mapProps((props, field) => {
return {
...props,
};
}),
mapReadPretty((props) => {
const prefixCls = usePrefixCls('description-color-picker', props);
return (
<div
className={cls(
prefixCls,
css`
display: inline-flex;
.ant-color-picker-trigger-disabled {
cursor: default;
}
`,
props.className,
)}
>
<AntdColorPicker disabled value={props.value} size="small" {...props} />
</div>
);
}),
);
export default ColorPicker;

View File

@ -0,0 +1,24 @@
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
import type { ColorPickerProps as AntdColorPickerProps } from 'antd/es/color-picker';
import cls from 'classnames';
import React from 'react';
type Composed = {
ColorPicker: React.FC<AntdColorPickerProps>;
};
export const ReadPretty: Composed = () => null;
ReadPretty.ColorPicker = function ColorPicker(props: any) {
const prefixCls = usePrefixCls('description-color-picker', props);
if (!props.value) {
return <div></div>;
}
return (
<div className={cls(prefixCls, props.className)}>
<ColorPicker showText disabled value={props.value} size='small'/>
</div>
);
};

View File

@ -0,0 +1,49 @@
/**
* title: ColorPicker
*/
import { FormItem } from '@formily/antd-v5';
import { ColorPicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const schema = {
type: 'object',
properties: {
input: {
type: 'boolean',
title: `Editable`,
'x-decorator': 'FormItem',
'x-component': 'ColorPicker',
'x-reactions': {
target: '*(read1,read2)',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read1: {
type: 'boolean',
title: `Read pretty`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'ColorPicker',
},
read2: {
type: 'string',
title: `Value`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
};
export default () => {
return (
<SchemaComponentProvider components={{ Input, ColorPicker, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};

View File

@ -0,0 +1,18 @@
---
group:
title: Schema Components
order: 3
---
# ColorPicker
## Examples
### Basic
<code src="./demos/demo1.tsx"></code>

View File

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

View File

@ -0,0 +1,158 @@
import { dayjs, getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client';
import type { Dayjs } from 'dayjs';
const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => {
if (!dayjs.isDayjs(value)) return value;
if (timezone === 'local') {
const offset = new Date().getTimezoneOffset();
return dayjs(toStringByPicker(value, picker, 'gmt')).add(offset, 'minutes').toISOString();
}
if (picker === 'year') {
return value.format('YYYY') + '-01-01T00:00:00.000Z';
}
if (picker === 'month') {
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
}
if (picker === 'quarter') {
return value.startOf('quarter').format('YYYY-MM') + '-01T00:00:00.000Z';
}
if (picker === 'week') {
return value.startOf('week').add(1, 'day').format('YYYY-MM-DD') + 'T00:00:00.000Z';
}
return value.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
};
const toGmtByPicker = (value: Dayjs, picker?: any) => {
if (!value || !dayjs.isDayjs(value)) {
return value;
}
return toStringByPicker(value, picker, 'gmt');
};
const toLocalByPicker = (value: Dayjs, picker?: any) => {
if (!value || !dayjs.isDayjs(value)) {
return value;
}
return toStringByPicker(value, picker, 'local');
};
export interface Moment2strOptions {
showTime?: boolean;
gmt?: boolean;
utc?: boolean;
picker?: 'year' | 'month' | 'week' | 'quarter';
}
export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}) => {
const { showTime, gmt, picker, utc = true } = options;
if (!value) {
return value;
}
if (!utc) {
const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
return value.format(format);
}
if (showTime) {
return gmt ? toGmt(value) : toLocal(value);
}
if (typeof gmt === 'boolean') {
return gmt ? toGmtByPicker(value, picker) : toLocalByPicker(value, picker);
}
return toGmtByPicker(value, picker);
};
export const mapDatePicker = function () {
return (props: any) => {
const format = getDefaultFormat(props) as any;
const onChange = props.onChange;
return {
...props,
format: format,
value: str2moment(props.value, props),
onChange: (value: Dayjs | null) => {
if (onChange) {
if (!props.showTime && value) {
value = value.startOf('day');
}
onChange(moment2str(value, props));
}
},
};
};
};
export const mapRangePicker = function () {
return (props: any) => {
const format = getDefaultFormat(props) as any;
const onChange = props.onChange;
return {
...props,
format: format,
value: str2moment(props.value, props),
onChange: (value: Dayjs[]) => {
if (onChange) {
onChange(
value
? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)]
: [],
);
}
},
} as any;
};
};
function getRangeStart(value: Dayjs, options: Moment2strOptions) {
const { showTime } = options;
if (showTime) {
return value;
}
return value.startOf('day');
}
function getRangeEnd(value: Dayjs, options: Moment2strOptions) {
const { showTime } = options;
if (showTime) {
return value;
}
return value.endOf('day');
}
const getStart = (offset: any, unit: any) => {
return dayjs()
.add(offset, unit === 'isoWeek' ? 'week' : unit)
.startOf(unit);
};
const getEnd = (offset: any, unit: any) => {
return dayjs()
.add(offset, unit === 'isoWeek' ? 'week' : unit)
.endOf(unit);
};
export const getDateRanges = () => {
return {
today: () => [getStart(0, 'day'), getEnd(0, 'day')],
lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')],
nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')],
lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')],
nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')],
lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')],
nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')],
last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')],
next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')],
last30Days: () => [getStart(-29, 'days'), getEnd(0, 'days')],
next30Days: () => [getStart(1, 'day'), getEnd(30, 'days')],
last90Days: () => [getStart(-89, 'days'), getEnd(0, 'days')],
next90Days: () => [getStart(1, 'day'), getEnd(90, 'days')],
};
};

View File

@ -28,6 +28,7 @@ import { BlockItem } from '../block-item';
import { removeNullCondition } from '../filter'; import { removeNullCondition } from '../filter';
import { HTMLEncode } from '../input/shared'; import { HTMLEncode } from '../input/shared';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent'; import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { useColorFields } from '../table-v2/Table.Column.Designer';
import { FilterFormDesigner } from './FormItem.FilterFormDesigner'; import { FilterFormDesigner } from './FormItem.FilterFormDesigner';
import { useEnsureOperatorsValid } from './SchemaSettingOptions'; import { useEnsureOperatorsValid } from './SchemaSettingOptions';
@ -183,6 +184,7 @@ FormItem.Designer = function Designer() {
value: field?.name, value: field?.name,
label: compile(field?.uiSchema?.title) || field?.name, label: compile(field?.uiSchema?.title) || field?.name,
})); }));
const colorFieldOptions = useColorFields(collectionField?.target ?? collectionField?.targetCollection);
let readOnlyMode = 'editable'; let readOnlyMode = 'editable';
if (fieldSchema['x-disabled'] === true) { if (fieldSchema['x-disabled'] === true) {
@ -529,10 +531,6 @@ FormItem.Designer = function Designer() {
schema['x-component-props'] = fieldSchema['x-component-props']; schema['x-component-props'] = fieldSchema['x-component-props'];
field.componentProps = field.componentProps || {}; field.componentProps = field.componentProps || {};
field.componentProps.mode = mode; field.componentProps.mode = mode;
// if (mode === 'Nester') {
// const initValue = ['hasMany', 'belongsToMany'].includes(collectionField?.type) ? [{}] : {};
// field.value = field.value || initValue;
// }
dn.emit('patch', { dn.emit('patch', {
schema, schema,
}); });
@ -811,6 +809,29 @@ FormItem.Designer = function Designer() {
/> />
)} )}
{isDateField && <SchemaSettings.DataFormat fieldSchema={fieldSchema} />} {isDateField && <SchemaSettings.DataFormat fieldSchema={fieldSchema} />}
{isAssociationField && ['Tag'].includes(fieldMode) && (
<SchemaSettings.SelectItem
key="title-field"
title={t('Tag color field')}
options={colorFieldOptions}
value={field?.componentProps?.tagColorField}
onChange={(tagColorField) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['tagColorField'] = tagColorField;
schema['x-component-props'] = fieldSchema['x-component-props'];
field.componentProps.tagColorField = tagColorField;
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
{collectionField && <SchemaSettings.Divider />} {collectionField && <SchemaSettings.Divider />}
<SchemaSettings.Remove <SchemaSettings.Remove
key="remove" key="remove"

View File

@ -9,6 +9,7 @@ export * from './card-item';
export * from './cascader'; export * from './cascader';
export * from './checkbox'; export * from './checkbox';
export * from './collection-select'; export * from './collection-select';
export * from './color-picker';
export * from './color-select'; export * from './color-select';
export * from './cron'; export * from './cron';
export * from './date-picker'; export * from './date-picker';
@ -49,4 +50,5 @@ export * from './time-picker';
export * from './tree-select'; export * from './tree-select';
export * from './upload'; export * from './upload';
export * from './variable'; export * from './variable';
import './index.less'; import './index.less';

View File

@ -27,6 +27,22 @@ const useLabelFields = (collectionName?: any) => {
}); });
}; };
export const useColorFields = (collectionName?: any) => {
const compile = useCompile();
const { getCollectionFields } = useCollectionManager();
if (!collectionName) {
return [];
}
const targetFields = getCollectionFields(collectionName);
return targetFields
?.filter?.((field) => field?.interface === 'color')
?.map?.((field) => {
return {
value: field.name,
label: compile(field?.uiSchema?.title || field.name),
};
});
};
export const TableColumnDesigner = (props) => { export const TableColumnDesigner = (props) => {
const { uiSchema, fieldSchema, collectionField } = props; const { uiSchema, fieldSchema, collectionField } = props;
const { getInterface, getCollection } = useCollectionManager(); const { getInterface, getCollection } = useCollectionManager();
@ -37,6 +53,7 @@ export const TableColumnDesigner = (props) => {
const fieldNames = const fieldNames =
fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['fieldNames']; fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['fieldNames'];
const options = useLabelFields(collectionField?.target ?? collectionField?.targetCollection); const options = useLabelFields(collectionField?.target ?? collectionField?.targetCollection);
const colorFieldOptions = useColorFields(collectionField?.target ?? collectionField?.targetCollection);
const intefaceCfg = getInterface(collectionField?.interface); const intefaceCfg = getInterface(collectionField?.interface);
const targetCollection = getCollection(collectionField?.target); const targetCollection = getCollection(collectionField?.target);
const isFileField = isFileCollection(targetCollection); const isFileField = isFileCollection(targetCollection);
@ -45,6 +62,7 @@ export const TableColumnDesigner = (props) => {
const defaultFilter = fieldSchema?.['x-component-props']?.service?.params?.filter || {}; const defaultFilter = fieldSchema?.['x-component-props']?.service?.params?.filter || {};
const dataSource = useCollectionFilterOptions(collectionField?.target); const dataSource = useCollectionFilterOptions(collectionField?.target);
const isDateField = ['datetime', 'createdAt', 'updatedAt'].includes(collectionField?.interface); const isDateField = ['datetime', 'createdAt', 'updatedAt'].includes(collectionField?.interface);
const fieldMode = fieldSchema?.['x-component-props']?.['mode'] || 'Select';
let readOnlyMode = 'editable'; let readOnlyMode = 'editable';
if (fieldSchema['x-disabled'] === true) { if (fieldSchema['x-disabled'] === true) {
readOnlyMode = 'readonly'; readOnlyMode = 'readonly';
@ -232,6 +250,55 @@ export const TableColumnDesigner = (props) => {
}} }}
/> />
)} )}
{readOnlyMode === 'read-pretty' &&
['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho', 'snapshot'].includes(collectionField?.interface) && (
<SchemaSettings.SelectItem
key="field-mode"
title={t('Field component')}
options={[
{ label: t('Title'), value: 'Select' },
{ label: t('Tag'), value: 'Tag' },
]}
value={fieldMode}
onChange={(mode) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['mode'] = mode;
schema['x-component-props'] = fieldSchema['x-component-props'];
field.componentProps = field.componentProps || {};
field.componentProps.mode = mode;
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
{['Tag'].includes(fieldMode) && (
<SchemaSettings.SelectItem
key="title-field"
title={t('Tag color field')}
options={colorFieldOptions}
value={fieldSchema?.['x-component-props']?.tagColorField}
onChange={(tagColorField) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['tagColorField'] = tagColorField;
schema['x-component-props'] = fieldSchema['x-component-props'];
field.componentProps.tagColorField = tagColorField;
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
{isSubTableColumn && !field.readPretty && !uiSchema?.['x-read-pretty'] && ( {isSubTableColumn && !field.readPretty && !uiSchema?.['x-read-pretty'] && (
<SchemaSettings.SwitchItem <SchemaSettings.SwitchItem

View File

@ -297,7 +297,6 @@ export const Table: any = observer(
console.warn('move cancel'); console.warn('move cancel');
return; return;
} }
const fromIndex = e.active?.data.current?.sortable?.index; const fromIndex = e.active?.data.current?.sortable?.index;
const toIndex = e.over?.data.current?.sortable?.index; const toIndex = e.over?.data.current?.sortable?.index;
const from = field.value[fromIndex]; const from = field.value[fromIndex];
@ -324,6 +323,11 @@ export const Table: any = observer(
.nb-read-pretty-input-number { .nb-read-pretty-input-number {
text-align: right; text-align: right;
} }
.ant-color-picker-trigger{
position:absolute;
top:50%;
transform: translateY(-50%);
}
`, `,
)} )}
/> />

View File

@ -24,6 +24,7 @@ export const useFieldModeOptions = () => {
? [ ? [
{ label: t('Title'), value: 'Select' }, { label: t('Title'), value: 'Select' },
{ label: t('File manager'), value: 'FileManager' }, { label: t('File manager'), value: 'FileManager' },
{ label: t('Tag'), value: 'Tag' },
] ]
: [ : [
{ label: t('Select'), value: 'Select' }, { label: t('Select'), value: 'Select' },
@ -37,6 +38,7 @@ export const useFieldModeOptions = () => {
return isReadPretty return isReadPretty
? [ ? [
{ label: t('Title'), value: 'Select' }, { label: t('Title'), value: 'Select' },
{ label: t('Tag'), value: 'Tag' },
{ label: t('Sub-table'), value: 'SubTable' }, { label: t('Sub-table'), value: 'SubTable' },
{ label: t('Sub-details'), value: 'Nester' }, { label: t('Sub-details'), value: 'Nester' },
] ]
@ -50,6 +52,7 @@ export const useFieldModeOptions = () => {
return isReadPretty return isReadPretty
? [ ? [
{ label: t('Title'), value: 'Select' }, { label: t('Title'), value: 'Select' },
{ label: t('Tag'), value: 'Tag' },
{ label: t('Sub-details'), value: 'Nester' }, { label: t('Sub-details'), value: 'Nester' },
{ label: t('Sub-table'), value: 'SubTable' }, { label: t('Sub-table'), value: 'SubTable' },
] ]
@ -64,6 +67,7 @@ export const useFieldModeOptions = () => {
return isReadPretty return isReadPretty
? [ ? [
{ label: t('Title'), value: 'Select' }, { label: t('Title'), value: 'Select' },
{ label: t('Tag'), value: 'Tag' },
{ label: t('Sub-details'), value: 'Nester' }, { label: t('Sub-details'), value: 'Nester' },
] ]
: [ : [
@ -76,6 +80,7 @@ export const useFieldModeOptions = () => {
return isReadPretty return isReadPretty
? [ ? [
{ label: t('Title'), value: 'Select' }, { label: t('Title'), value: 'Select' },
{ label: t('Tag'), value: 'Tag' },
{ label: t('Sub-details'), value: 'Nester' }, { label: t('Sub-details'), value: 'Nester' },
] ]
: [ : [

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { ArrayCollapse, ArrayItems, FormItem, FormLayout, Input } from '@formily/antd-v5'; import { ArrayCollapse, ArrayItems, FormItem, FormLayout, Input } from '@formily/antd-v5';
import { Field, GeneralField, createForm } from '@formily/core'; import { createForm, Field, GeneralField } from '@formily/core';
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react'; import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { error } from '@nocobase/utils/client'; import { error } from '@nocobase/utils/client';
@ -21,32 +21,32 @@ import {
} from 'antd'; } from 'antd';
import _, { cloneDeep } from 'lodash'; import _, { cloneDeep } from 'lodash';
import React, { import React, {
ReactNode,
createContext, createContext,
ReactNode,
useCallback, useCallback,
useContext, useContext,
useMemo, useMemo,
useState,
// @ts-ignore // @ts-ignore
useTransition as useReactTransition, useTransition as useReactTransition,
useState,
} from 'react'; } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
APIClientProvider,
ActionContextProvider, ActionContextProvider,
APIClientProvider,
CollectionFieldOptions, CollectionFieldOptions,
CollectionManagerContext, CollectionManagerContext,
CollectionProvider, CollectionProvider,
createDesignable,
Designable, Designable,
findFormBlock,
FormDialog, FormDialog,
FormProvider, FormProvider,
RemoteSchemaComponent, RemoteSchemaComponent,
SchemaComponent, SchemaComponent,
SchemaComponentContext, SchemaComponentContext,
SchemaComponentOptions, SchemaComponentOptions,
createDesignable,
findFormBlock,
useAPIClient, useAPIClient,
useBlockRequestContext, useBlockRequestContext,
useCollection, useCollection,