refactor(association-field): support sub table (#1862)

* fix: association select support data scope and sort setting

* fix: appends on demand loading

* fix: asociationSelect support multiple

* fix: recordPicker -> AssociationField

* fix: add migration

* fix: audit logs not show assication data

* fix: flattenNestedList

* refactor: file manager field mode

* refactor: field mode refactor

* fix: subtable action

* fix: subtable appends

* refactor: code improve

* fix: nester add new

* feat: sub table

* fix: data scope not effect immediately

* fix: association add new

* fix: association field failed to  add new and mutual influence

* style: style improve

* style: style improve

* refactor: updateAssociationValues

* refactor: form init values

* refactor: select options

* fix: form initialValues

* fix: record picker values

* fix: field value change when field mode change

* fix: select data scope

* feat: add migration

* fix: table column enable link

* fix: table column enable link

* refactor: locale improve

* fix: migration

* fix: mutiple config

* fix: readPretty enable link

* fix: appends on demand

* fix: enable link style

* refactor: locale improve

* refactor: locale improve

* feat: sub-form migration

* fix: skip migration

* fix: translation

* fix: skip migration

* fix: getLabelFormatValue

* fix: error TS2339: Property 'find' does not exist on type 'string | SchemaEnum<any>'

* refactor: remove the logic code for converting old record picker

* refactor: locale

* refactor: association field support sub-table

* refactor: locale

* fix: sub-table should not support add new

* refactor: code improve

* refactor: locale

* fix: compatibility history Subtable

* fix: getNesterAppends filter bug

* refactor: useFieldModeOptions

* refactor: subTable

* fix: associationSelect

* fix: associationSelect

* fix: associationSelect

* fix: getAssociationAppends

* fix: sub-form collection context

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* fix: sub-table

* fix: appends

* fix: sub-table-appends

* fix: undefine id

* fix: sub-table appends

* refactor: sub-table

* refactor: sub-table supoort delete

* fix: sub-table-appends

* refactor: sub table initvvalue

* fix: sub-table-appends

* fix:  association field appends

* fix:  association field appends

* fix:  association field appends

* style: style improve

* style: style improve

* refactor: textArear Markdown RichText

* style: style improve

* fix: title field

* refactor: code improve

* fix: system fields are not allowed to be edited

* refactor: subtable does not support inline operations

* refactor: jons edit in table

* refactor: sequence readOnly in table

* refactor: json readOnly in table

* fix: sub-table appends

* fix: runWhenParamsChanged

* fix: runWhenParamsChanged

* fix: sub-table appends

* fix: sub-table appends

* refactor: sub-table

* style: checkbox group intable

* style: delete button style improve

* fix: json edit in table

* fix: sub-table appends

* style: upload file in table column

* refactor: map edit in table

* refactor: map edit in table

* refactor: map field display in sub-table

* fix: support mutiple row

* refactor: sub-table

* refactor: sub-table not support sortable

* fix: sub-table appends

* fix: sub-table appends

* chore: useAssociationNames

* refactor: code improve

* refactor: support blocktemplate

* refactor: support blocktemplate

* fix: subtable

* fix: subtable initValue

* refactor: useAssociationNames in block template

* fix: cross hierarchical association field layout

* fix: normal field of association field should be append

* style: upload field in table

* refactor: sub table should not row selection

* style: checkboxGroup and radioGroup display in sub-table

* refactor: locale

* refactor: code improve

* refactor: quickEdit in sub-table

* refactor: quickEdit

* refactor: quickEdit

* refactor: quickEdit

* refactor: quickEdit

* style: quickEdit style improve

* style: quickEdit style improve

* style: quickEdit style improve

* refactor: sub-table support required config

* refactor: cell validate

* refactor: cell validate

* refactor: cell required validate

* refactor: cell required validate

* refactor: sub-table display when readPretty

* refactor: sub-table required

* refactor: sub-table required

* refactor: sub-table required

* refactor: sub-table initialValue

* refactor: table

* refactor: sub-tble field  required

* refactor: sub-table style improve

* refactor: sub-table attachemnt

* refactor: code improve

* test: tableV2 snap

* fix: subtable drag

* test: tableV2 snap

* refactor: checkbox group and radio group in sub-table

* refactor: sub-table required style

* style: markdown style in sub-table

* refactor: markdown display in sub-table

* refactor: json field display in sub-table

* refactor: code improve

* style: style improve

* style: style improve

* refactor: inserView when designable

* refactor:  china-region load data when dropdown click

* refactor: code improve

* refactor: code improve

* refactor: display loading when the form is first loaded

* refactor: display loading when the form is first loaded

* fix: verification exception when deleting sub table rows

* refactor: sub-table code improve

* refactor: sub-table code improve

* fix: json error feedback

* refactor: code improve

* fix: json onchange

* fix:  sub table data did not change when switching data templates

* test: variable test

* test: variable test

* refactor: data template

* refactor: code improve

* refactor: code improve

* fix: upload readpretty

* fix: attach upload in sub-tabl

* fix: remove small

* fix: attach upload in sub-table

* refactor: association fields should not display in sub-table

* fix: dropdownMatchSelectWidth

* refactor: association fields should not display in sub-table

* refactor: selectFile is false in sub-table

* refactor: attacattachment in sub-table

* refactor: attacattachment in sub-table

* fix: selector style

* refactor: quick edit component

* fix: zindex

* style: style improve

* style: style improve

* feat: styling

* fix: style

* fix: style

* refactor: quickEdit getContainer

* refactor: quickEdit

* refactor: quickEdit

* fix: improve code

* refactor: code improve

* refactor: code improve

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
katherinehhh 2023-06-16 12:07:43 +08:00 committed by GitHub
parent 457afe3e06
commit 4faf64f905
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 783 additions and 1178 deletions

View File

@ -1,6 +1,7 @@
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import { useCollection } from '../collection-manager'; import { useCollection } from '../collection-manager';
import { RecordProvider, useRecord } from '../record-provider'; import { RecordProvider, useRecord } from '../record-provider';
@ -23,9 +24,9 @@ const InternalFormBlockProvider = (props) => {
const { resource, service, updateAssociationValues } = useBlockRequestContext(); const { resource, service, updateAssociationValues } = useBlockRequestContext();
const formBlockRef = useRef(); const formBlockRef = useRef();
const record = useRecord(); const record = useRecord();
// if (service.loading) { if (service.loading && Object.keys(form?.initialValues)?.length === 0 && action) {
// return <Spin />; return <Spin />;
// } }
return ( return (
<FormBlockContext.Provider <FormBlockContext.Provider
value={{ value={{

View File

@ -127,7 +127,7 @@ export const TableBlockProvider = (props) => {
return ( return (
<SchemaComponentOptions scope={{ treeTable }}> <SchemaComponentOptions scope={{ treeTable }}>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<BlockProvider {...props} params={params}> <BlockProvider {...props} params={params} runWhenParamsChanged>
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} /> <InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
</BlockProvider> </BlockProvider>
</FormContext.Provider> </FormContext.Provider>

View File

@ -46,7 +46,8 @@ const InternalField: React.FC = (props: Props) => {
setFieldProps('title', uiSchema.title); setFieldProps('title', uiSchema.title);
setFieldProps('description', uiSchema.description); setFieldProps('description', uiSchema.description);
if (ctx?.form) { if (ctx?.form) {
setFieldProps('initialValue', fieldSchema.default || defaultValue); const defaultVal = fieldSchema.default || defaultValue;
defaultVal !== null && defaultVal !== undefined && setFieldProps('initialValue', defaultVal);
} }
if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) { if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) {

View File

@ -12,16 +12,10 @@ export const url: IField = {
type: 'string', type: 'string',
uiSchema: { uiSchema: {
type: 'string', type: 'string',
title: '{{t("URL")}}',
'x-component': 'Input.URL', 'x-component': 'Input.URL',
}, },
}, },
schemaInitialize(schema: ISchema, { block }) { schemaInitialize(schema: ISchema, { block }) {},
if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {};
schema['x-component-props']['size'] = 'small';
}
},
properties: { properties: {
...defaultProps, ...defaultProps,
}, },

View File

@ -293,14 +293,14 @@ export default {
"Allow multiple": "Allow multiple", "Allow multiple": "Allow multiple",
"Quick upload": "Quick upload", "Quick upload": "Quick upload",
"Select file": "Select file", "Select file": "Select file",
"Subtable": "Subtable", "Subtable": "Sub-table",
"Subform": "Sub-form", "Subform": "Sub-form",
"Field mode":"Field mode", "Field mode":"Field mode",
"Allow add new data":"Allow add new data", "Allow add new data":"Allow add new data",
"Record picker": "Record picker", "Record picker": "Record picker",
"Toggles the subfield mode": "Toggles the subfield mode", "Toggles the subfield mode": "Toggles the subfield mode",
"Selector mode": "Selector mode", "Selector mode": "Selector mode",
"Subtable mode": "Subtable mode", "Subtable mode": "Sub-table mode",
"Subform mode": "Sub-form mode", "Subform mode": "Sub-form mode",
"Edit block title": "Edit block title", "Edit block title": "Edit block title",
"Block title": "Block title", "Block title": "Block title",

View File

@ -17,7 +17,7 @@ export const ActionDrawer: ComposedActionDrawer = observer(
(props) => { (props) => {
const { footerNodeName = 'Action.Drawer.Footer', ...others } = props; const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext(); const { visible, setVisible, openSize = 'middle', drawerProps, modalProps } = useActionContext();
const schema = useFieldSchema(); const schema = useFieldSchema();
const field = useField(); const field = useField();
const openSizeFromParent = schema.parent?.['x-component-props']?.['openSize']; const openSizeFromParent = schema.parent?.['x-component-props']?.['openSize'];
@ -35,6 +35,7 @@ export const ActionDrawer: ComposedActionDrawer = observer(
title={field.title} title={field.title}
{...others} {...others}
{...drawerProps} {...drawerProps}
{...modalProps}
style={{ style={{
...drawerProps?.style, ...drawerProps?.style,
...others?.style, ...others?.style,

View File

@ -84,6 +84,9 @@ export const AssociationFieldProvider = observer(
field.value = [{}]; field.value = [{}];
} }
} }
if (currentMode === 'SubTable') {
field.value = [];
}
setLoading(false); setLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentMode, collectionField, fieldValue]); }, [currentMode, collectionField, fieldValue]);

View File

@ -70,6 +70,7 @@ const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
<RemoteSelect <RemoteSelect
style={{ width: '100%' }} style={{ width: '100%' }}
{...props} {...props}
size={'middle'}
objectValue={objectValue} objectValue={objectValue}
value={value} value={value}
service={service} service={service}

View File

@ -133,7 +133,7 @@ const InternalFileManager = (props) => {
}; };
}; };
return ( return (
<div> <div style={{ width: '100%', overflow: 'auto' }}>
<FileSelector <FileSelector
value={options} value={options}
multiple={multiple} multiple={multiple}
@ -153,7 +153,16 @@ const InternalFileManager = (props) => {
} }
}} }}
/> />
<ActionContextProvider value={{ openMode: 'drawer', visible: visibleSelector, setVisible: setVisibleSelector }}> <ActionContextProvider
value={{
openMode: 'drawer',
visible: visibleSelector,
setVisible: setVisibleSelector,
modalProps: {
getContainer: others?.getContainer,
},
}}
>
<RecordPickerProvider {...pickerProps}> <RecordPickerProvider {...pickerProps}>
<CollectionProvider name={collectionField.target}> <CollectionProvider name={collectionField.target}>
<FormProvider> <FormProvider>

View File

@ -1,21 +1,51 @@
import { FormLayout } from '@formily/antd'; import { css } from '@emotion/css';
import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { FormItem, FormLayout } from '@formily/antd';
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { CollectionProvider } from '../../../collection-manager'; import { CollectionProvider } from '../../../collection-manager';
import { SchemaComponentOptions } from '../../../schema-component';
import Select from '../select/Select';
import { useAssociationFieldContext, useInsertSchema } from './hooks'; import { useAssociationFieldContext, useInsertSchema } from './hooks';
import schema from './schema'; import schema from './schema';
export const InternalSubTable = () => { export const InternalSubTable = observer(
const field = useField(); () => {
const field: any = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const insert = useInsertSchema('SubTable'); const insert = useInsertSchema('SubTable');
const { options } = useAssociationFieldContext(); const { options } = useAssociationFieldContext();
useEffect(() => { useEffect(() => {
insert(schema.SubTable); insert(schema.SubTable);
field.required = fieldSchema['required'];
}, []); }, []);
return ( return (
<CollectionProvider name={options.target}> <CollectionProvider name={options.target}>
<FormLayout layout={'vertical'}> <FormLayout
className={css`
.ant-formily-item-bordered-none {
.ant-input-number-group-addon {
border: none !important;
background: none;
}
.ant-checkbox-wrapper {
margin-left: 8px;
}
.ant-btn {
border: none !important;
box-shadow: none;
}
}
`}
layout={'vertical'}
bordered={false}
>
<SchemaComponentOptions
components={{
FormItem: (props) => <FormItem {...props} />,
'Radio.Group': Select,
'Checkbox.Group': (props) => <Select multiple={true} mode="multiple" {...props} />,
}}
>
<RecursionField <RecursionField
onlyRenderProperties onlyRenderProperties
basePath={field.address} basePath={field.address}
@ -24,7 +54,10 @@ export const InternalSubTable = () => {
return s['x-component'] === 'AssociationField.SubTable'; return s['x-component'] === 'AssociationField.SubTable';
}} }}
/> />
</SchemaComponentOptions>
</FormLayout> </FormLayout>
</CollectionProvider> </CollectionProvider>
); );
}; },
{ displayName: 'InternalSubTable' },
);

View File

@ -6,6 +6,7 @@ import { CollectionProvider } from '../../../collection-manager';
import { RecordProvider, useRecord } from '../../../record-provider'; import { RecordProvider, useRecord } from '../../../record-provider';
import { FormProvider } from '../../core'; import { FormProvider } from '../../core';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
import { useDesignable } from '../../';
import { ActionContextProvider, useActionContext } from '../action'; import { ActionContextProvider, useActionContext } from '../action';
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip'; import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks'; import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
@ -35,6 +36,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
const { options: collectionField } = useAssociationFieldContext(); const { options: collectionField } = useAssociationFieldContext();
const [record, setRecord] = useState({}); const [record, setRecord] = useState({});
const compile = useCompile(); const compile = useCompile();
const { designable } = useDesignable();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label'); const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const { snapshot } = useActionContext(); const { snapshot } = useActionContext();
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>(); const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
@ -53,7 +55,9 @@ export const ReadPrettyInternalViewer: React.FC = observer(
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (designable) {
insertViewer(schema.Viewer); insertViewer(schema.Viewer);
}
setVisible(true); setVisible(true);
setRecord(record); setRecord(record);
ellipsisWithTooltipRef?.current?.setPopoverVisible(false); ellipsisWithTooltipRef?.current?.setPopoverVisible(false);

View File

@ -1,29 +1,111 @@
import { PlusOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { exchangeArrayState } from '@formily/core/esm/shared/internals';
import { observer } from '@formily/react'; import { observer } from '@formily/react';
import { action } from '@formily/reactive';
import { isArr } from '@formily/shared';
import { Button } from 'antd'; import { Button } from 'antd';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { Table } from '../table-v2/Table'; import { Table } from '../table-v2/Table';
import { useAssociationFieldContext } from './hooks'; import { useAssociationFieldContext } from './hooks';
export const SubTable: any = observer( export const SubTable: any = observer(
(props) => { (props: any) => {
const { field } = useAssociationFieldContext<ArrayField>(); const { field } = useAssociationFieldContext<ArrayField>();
const { t } = useTranslation();
const move = (fromIndex: number, toIndex: number) => {
if (toIndex === undefined) return;
if (!isArr(field.value)) return;
if (fromIndex === toIndex) return;
return action(() => {
const fromItem = field.value[fromIndex];
field.value.splice(fromIndex, 1);
field.value.splice(toIndex, 0, fromItem);
exchangeArrayState(field, {
fromIndex,
toIndex,
});
return field.onInput(field.value);
});
};
field.move = move;
return ( return (
<div> <div
<Table size={'small'} showIndex field={field} pagination={false} /> className={css`
{field.editable && ( .ant-table-footer {
padding: 0 !important;
}
.ant-formily-item-error-help {
display: none;
}
.ant-description-textarea {
line-height: 34px;
}
.ant-table-cell .ant-formily-item-error-help {
display: block;
position: absolute;
font-size: 12px;
top: 100%;
background: #fff;
width: 100%;
margin-top: 3px;
padding: 3px;
z-index: 1;
border-radius: 3px;
box-shadow: 0 0 10px #eee;
animation: none;
transform: translateY(0);
opacity: 1;
}
`}
>
<Table
className={css`
.ant-select-selector {
/* min-height: 31px; */
}
.ant-formily-item.ant-formily-item-feedback-layout-loose {
margin-bottom: 0px !important;
}
.ant-formily-editable {
vertical-align: sub;
}
.ant-table table {
}
td:not(.ant-table-selection-column) {
padding: 0 !important;
}
`}
bordered
size={'small'}
field={field}
showIndex
dragSort={field.editable}
showDel={field.editable}
pagination={false}
rowSelection={{ type: 'none', hideSelectAll: true }}
footer={() =>
field.editable && (
<Button <Button
type={'dashed'} type={'text'}
block block
style={{ marginTop: 12 }} className={css`
display: block;
`}
onClick={() => { onClick={() => {
field.value = field.value || []; field.value = field.value || [];
field.value.push({}); field.value.push({});
field.onInput(field.value);
}} }}
icon={<PlusOutlined />}
> >
Add new {/* {t('Add new')} */}
</Button> </Button>
)} )
}
/>
</div> </div>
); );
}, },

View File

@ -121,21 +121,9 @@ export default {
type: 'void', type: 'void',
'x-component': 'AssociationField.SubTable', 'x-component': 'AssociationField.SubTable',
'x-initializer': 'TableColumnInitializers', 'x-initializer': 'TableColumnInitializers',
properties: { 'x-initializer-props': {
indexCol: { action: false,
type: 'void',
'x-component': 'TableV2.Column',
'x-component-props': {
width: 80,
},
properties: {
__index__: {
type: 'void',
'x-component': 'TableV2.Index',
'x-read-pretty': true,
},
},
},
}, },
properties: {},
}, },
}; };

View File

@ -2,9 +2,10 @@ import { LoadingOutlined } from '@ant-design/icons';
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { connect, mapProps, mapReadPretty, useField } from '@formily/react'; import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
import { toArr } from '@formily/shared'; import { toArr } from '@formily/shared';
import { action } from '@formily/reactive';
import { Cascader as AntdCascader, Space } from 'antd'; import { Cascader as AntdCascader, Space } from 'antd';
import { isBoolean, omit } from 'lodash'; import { isBoolean, omit } from 'lodash';
import React from 'react'; import React, { useState } from 'react';
import { useRequest } from '../../../api-client'; import { useRequest } from '../../../api-client';
import { defaultFieldNames } from './defaultFieldNames'; import { defaultFieldNames } from './defaultFieldNames';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
@ -35,7 +36,7 @@ export const Cascader = connect(
} = props; } = props;
const fieldNames = { ...defaultFieldNames, ...props.fieldNames }; const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
const loadData = useLoadData(props); const loadData = useLoadData(props);
const { loading } = useDataSource({ const { loading, run } = useDataSource({
onSuccess(data) { onSuccess(data) {
field.dataSource = data?.data || []; field.dataSource = data?.data || [];
}, },
@ -62,6 +63,11 @@ export const Cascader = connect(
</Space> </Space>
); );
}; };
const handelDropDownVisible = (value) => {
if (value && !field.dataSource) {
run();
}
};
return ( return (
<AntdCascader <AntdCascader
loading={loading} loading={loading}
@ -72,6 +78,7 @@ export const Cascader = connect(
value={toValue()} value={toValue()}
fieldNames={fieldNames} fieldNames={fieldNames}
displayRender={displayRender} displayRender={displayRender}
onDropdownVisibleChange={handelDropDownVisible}
onChange={(value, selectedOptions) => { onChange={(value, selectedOptions) => {
if (value && labelInValue) { if (value && labelInValue) {
onChange(selectedOptions.map((option) => omit(option, [fieldNames.children]))); onChange(selectedOptions.map((option) => omit(option, [fieldNames.children])));

View File

@ -833,7 +833,7 @@ FormItem.Designer = function Designer() {
}} }}
/> />
)} )}
{options.length > 0 && isAssociationField && ( {options.length > 0 && isAssociationField&&fieldMode!=='SubTable' && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
key="title-field" key="title-field"
title={t('Title field')} title={t('Title field')}

View File

@ -72,13 +72,21 @@ const useDataTemplates = () => {
enabled: items.length > 0 && items.every((item) => item.dataId !== undefined), enabled: items.length > 0 && items.every((item) => item.dataId !== undefined),
}; };
}; };
function filterReferences(obj) {
const filteredObj = {};
for (const key in obj) {
if (typeof obj[key] !== 'object') {
filteredObj[key] = obj[key];
}
}
return filteredObj;
}
export const Templates = ({ style = {}, form }) => { export const Templates = ({ style = {}, form }) => {
const { templates, display, enabled, defaultTemplate } = useDataTemplates(); const { templates, display, enabled, defaultTemplate } = useDataTemplates();
const [value, setValue] = React.useState(defaultTemplate?.key || 'none'); const [value, setValue] = React.useState(defaultTemplate?.key || 'none');
const api = useAPIClient(); const api = useAPIClient();
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (enabled && defaultTemplate) { if (enabled && defaultTemplate) {
form.__template = true; form.__template = true;

View File

@ -31,6 +31,12 @@
} }
} }
.ant-formily-item-bordered-none {
.ant-formily-item-feedback-layout-loose {
margin-bottom: 0 !important;
}
}
.ant-formily-item-control-content-component .ant-tag { .ant-formily-item-control-content-component .ant-tag {
white-space: pre-wrap; white-space: pre-wrap;
} }

View File

@ -45,4 +45,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';
export * from './quick-edit'
import './index.less'; import './index.less';

View File

@ -35,11 +35,14 @@ export const Json = React.forwardRef<typeof Input.TextArea, JSONTextAreaProps>(
onChange={(ev) => { onChange={(ev) => {
setText(ev.target.value); setText(ev.target.value);
try { try {
const v = ev.target.value.trim() !== '' ? JSON.parse(ev.target.value) : null;
if (ev.target.value.trim() !== '') { if (ev.target.value.trim() !== '') {
JSON.parse(ev.target.value); JSON.parse(ev.target.value);
} }
field.setFeedback({}); field.setFeedback({});
onChange?.(v);
} catch (err) { } catch (err) {
onChange?.(ev);
field.setFeedback({ field.setFeedback({
type: 'error', type: 'error',
code: 'JSONSyntaxError', code: 'JSONSyntaxError',
@ -53,6 +56,7 @@ export const Json = React.forwardRef<typeof Input.TextArea, JSONTextAreaProps>(
field.setFeedback({}); field.setFeedback({});
onChange?.(v); onChange?.(v);
} catch (err) { } catch (err) {
onChange?.(ev);
field.setFeedback({ field.setFeedback({
type: 'error', type: 'error',
code: 'JSONSyntaxError', code: 'JSONSyntaxError',

View File

@ -20,16 +20,18 @@ export const Markdown: any = connect(
suffix: <span>{field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffix}</span>, suffix: <span>{field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffix}</span>,
}; };
}), }),
mapReadPretty((props) => { mapReadPretty((props) => <MarkdownReadPretty {...props} />),
const { html, loading } = useParseMarkdown(props.value); );
export const MarkdownReadPretty = (props) => {
const { html = '', loading } = useParseMarkdown(props.value);
const text = convertToText(html); const text = convertToText(html);
const value = <div className={'nb-markdown'} dangerouslySetInnerHTML={{ __html: html }} />; const value = <div className={'nb-markdown'} dangerouslySetInnerHTML={{ __html: html }} />;
if (loading) { if (loading) {
return <Spin />; return <Spin />;
} }
return <InputReadPretty.TextArea {...props} autop={false} text={text} value={value} />; return <InputReadPretty.TextArea {...props} autop={false} text={text} value={value} />;
}), };
);
Markdown.Void = MarkdownVoid; Markdown.Void = MarkdownVoid;

View File

@ -2,6 +2,7 @@ import { DeleteOutlined, DownloadOutlined, PlusOutlined } from '@ant-design/icon
import { connect, mapReadPretty } from '@formily/react'; import { connect, mapReadPretty } from '@formily/react';
import { Upload as AntdUpload, Button, Progress, Space } from 'antd'; import { Upload as AntdUpload, Button, Progress, Space } from 'antd';
import cls from 'classnames'; import cls from 'classnames';
import { css } from '@emotion/css';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -44,7 +45,6 @@ export const FileSelector = (props: Props) => {
onRemove?.(file); onRemove?.(file);
return true; return true;
}; };
const handleSelect = (e) => { const handleSelect = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View File

@ -0,0 +1,78 @@
import { css } from '@emotion/css';
import { FormItem } from '@formily/antd';
import { Field, createForm } from '@formily/core';
import { FormContext, RecursionField, observer, useField, useFieldSchema } from '@formily/react';
import { Popover } from 'antd';
import React, { useMemo, useRef } from 'react';
import { useCollectionManager } from '../../../collection-manager';
export const Editable = observer((props) => {
const field: any = useField();
const containerRef = useRef(null);
const fieldSchema = useFieldSchema();
const schema: any = {
name: fieldSchema.name,
'x-collection-field': fieldSchema['x-collection-field'],
'x-component': 'CollectionField',
'x-read-pretty': true,
default: field.value,
'x-component-props': fieldSchema['x-component-props'],
};
const form = useMemo(
() =>
createForm({
values: {
[fieldSchema.name]: field.value,
},
}),
[field.value, fieldSchema['x-component-props']],
);
const getContainer = () => {
return containerRef.current;
};
const modifiedChildren = React.Children.map(props.children, (child) => {
if (React.isValidElement(child)) {
//@ts-ignore
return React.cloneElement(child, { getContainer });
}
return child;
});
return (
<FormItem {...props} labelStyle={{ display: 'none' }}>
<Popover
content={
<div style={{ width: '100%', height: '100%', minWidth: 500 }}>
<div ref={containerRef}>{modifiedChildren}</div>
</div>
}
trigger="click"
placement={'bottomLeft'}
overlayClassName={css`
padding-top: 0;
.ant-popover-arrow {
display: none;
}
`}
>
<div style={{ minHeight: 30, padding: '0 8px' }}>
<FormContext.Provider value={form}>
<RecursionField schema={schema} name={fieldSchema.name} />
</FormContext.Provider>
</div>
</Popover>
</FormItem>
);
});
export const QuickEdit = observer((props) => {
const field = useField<Field>();
const { getCollectionJoinField } = useCollectionManager();
const fieldSchema = useFieldSchema();
const collectionField = getCollectionJoinField(fieldSchema['x-collection-field']);
if (!collectionField) {
return null;
}
return field.editable ? <Editable {...props} /> : <FormItem {...props} style={{ padding: '0 8px' }} />;
});

View File

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

View File

@ -235,9 +235,10 @@ const InternalRemoteSelect = connect(
dataSource: 'options', dataSource: 'options',
}, },
(props, field) => { (props, field) => {
const fieldSchema = useFieldSchema();
return { return {
...props, ...props,
fieldNames: { ...defaultFieldNames, ...props.fieldNames, ...field.componentProps.fieldNames }, fieldNames: { ...defaultFieldNames, ...props.fieldNames, ...field.componentProps.fieldNames,...fieldSchema['x-component-props']?.fieldNames },
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon, suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
}; };
}, },

View File

@ -2,8 +2,9 @@ import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollectionManager } from '../../../collection-manager'; import { useCollectionManager } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled } from '../../../schema-settings';
import { useCompile, useDesignable } from '../../hooks'; import { useCompile, useDesignable } from '../../hooks';
import { useAssociationFieldContext } from '../association-field/hooks';
const useLabelFields = (collectionName?: any) => { const useLabelFields = (collectionName?: any) => {
// 需要在组件顶层调用 // 需要在组件顶层调用
@ -26,7 +27,7 @@ const useLabelFields = (collectionName?: any) => {
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();
const field = useField(); const field: any = useField();
const { t } = useTranslation(); const { t } = useTranslation();
const columnSchema = useFieldSchema(); const columnSchema = useFieldSchema();
const { dn } = useDesignable(); const { dn } = useDesignable();
@ -36,6 +37,15 @@ export const TableColumnDesigner = (props) => {
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);
const isSubTableColumn = ['QuickEdit', 'FormItem'].includes(fieldSchema['x-decorator']);
const { currentMode, field: tableField } = useAssociationFieldContext();
let readOnlyMode = 'editable';
if (fieldSchema['x-disabled'] === true) {
readOnlyMode = 'readonly';
}
if (fieldSchema['x-read-pretty'] === true) {
readOnlyMode = 'read-pretty';
}
return ( return (
<GeneralSchemaDesigner disableInitializer> <GeneralSchemaDesigner disableInitializer>
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
@ -101,7 +111,7 @@ export const TableColumnDesigner = (props) => {
dn.refresh(); dn.refresh();
}} }}
/> />
{intefaceCfg && intefaceCfg.sortable === true && ( {intefaceCfg && intefaceCfg.sortable === true && !currentMode && (
<SchemaSettings.SwitchItem <SchemaSettings.SwitchItem
title={t('Sortable')} title={t('Sortable')}
checked={field.componentProps.sorter} checked={field.componentProps.sorter}
@ -125,7 +135,8 @@ export const TableColumnDesigner = (props) => {
{['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho', 'snapshot', 'createdBy', 'updatedBy'].includes( {['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho', 'snapshot', 'createdBy', 'updatedBy'].includes(
collectionField?.interface, collectionField?.interface,
) && ) &&
!isFileField && ( !isFileField &&
field.readPrety && (
<SchemaSettings.SwitchItem <SchemaSettings.SwitchItem
title={t('Enable link')} title={t('Enable link')}
checked={fieldSchema['x-component-props']?.enableLink !== false} checked={fieldSchema['x-component-props']?.enableLink !== false}
@ -177,6 +188,98 @@ export const TableColumnDesigner = (props) => {
}} }}
/> />
)} )}
{isSubTableColumn && !field.readPretty && !uiSchema?.['x-read-pretty'] && (
<SchemaSettings.SwitchItem
key="required"
title={t('Required')}
checked={fieldSchema.required as boolean}
onChange={(required) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
fieldSchema['required'] = required;
schema['required'] = required;
const path = field.path?.splice(field.path?.length - 1, 1);
(tableField as any)?.value.map((_, index) => {
field.form.query(`${path.concat(`${index}.` + fieldSchema.name)}`).take((f) => {
f.required = required;
});
});
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
{isSubTableColumn &&
!field?.readPretty &&
collectionField?.interface !== 'o2m' &&
!isPatternDisabled(fieldSchema) && (
<SchemaSettings.SelectItem
key="pattern"
title={t('Pattern')}
options={[
{ label: t('Editable'), value: 'editable' },
{ label: t('Readonly'), value: 'readonly' },
{ label: t('Easy-reading'), value: 'read-pretty' },
]}
value={readOnlyMode}
onChange={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
const path = field.path?.splice(field.path?.length - 1, 1);
switch (v) {
case 'readonly': {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = true;
schema['x-read-pretty'] = false;
schema['x-disabled'] = true;
(tableField as any)?.value.map((_, index) => {
field.form.query(`${path.concat(`${index}.` + fieldSchema.name)}`).take((f) => {
f.readonly = true;
f.disabled = true;
});
});
break;
}
case 'read-pretty': {
fieldSchema['x-read-pretty'] = true;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = true;
schema['x-disabled'] = false;
(tableField as any)?.value.map((_, index) => {
field.form.query(`${path.concat(`${index}.` + fieldSchema.name)}`).take((f) => {
f.readPretty = true;
});
});
break;
}
default: {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = false;
schema['x-disabled'] = false;
(tableField as any)?.value.map((_, index) => {
field.form.query(`${path.concat(`${index}.` + fieldSchema.name)}`).take((f) => {
f.readPretty = false;
f.disabled + false;
});
});
break;
}
}
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
<SchemaSettings.Divider /> <SchemaSettings.Divider />
<SchemaSettings.Remove <SchemaSettings.Remove
removeParentsIfNoChildren removeParentsIfNoChildren

View File

@ -1,9 +1,10 @@
import { MenuOutlined } from '@ant-design/icons'; import { DeleteOutlined, MenuOutlined } from '@ant-design/icons';
import { SortableContext, useSortable } from '@dnd-kit/sortable'; import { SortableContext, useSortable } from '@dnd-kit/sortable';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { ArrayField, Field } from '@formily/core'; import { ArrayField, Field } from '@formily/core';
import { spliceArrayState } from '@formily/core/esm/shared/internals';
import { RecursionField, Schema, observer, useField, useFieldSchema } from '@formily/react'; import { RecursionField, Schema, observer, useField, useFieldSchema } from '@formily/react';
import { reaction } from '@formily/reactive'; import { action, reaction } from '@formily/reactive';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Table as AntdTable, TableColumnProps } from 'antd'; import { Table as AntdTable, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames'; import { default as classNames, default as cls } from 'classnames';
@ -71,12 +72,41 @@ const useTableColumns = (props) => {
if (!exists) { if (!exists) {
return columns; return columns;
} }
return columns.concat({
const tableColumns = columns.concat({
title: render(), title: render(),
dataIndex: 'TABLE_COLUMN_INITIALIZER', dataIndex: 'TABLE_COLUMN_INITIALIZER',
key: 'TABLE_COLUMN_INITIALIZER', key: 'TABLE_COLUMN_INITIALIZER',
render: designable ? () => <div style={{ minWidth: 300 }} /> : null, render: designable ? () => <div style={{ minWidth: 300 }} /> : null,
}); });
if (props.showDel) {
tableColumns.push({
title: '',
key: 'delete',
width: 60,
align: 'center',
fixed: 'right',
render: (v, record, index) => {
return (
<DeleteOutlined
style={{ cursor: 'pointer' }}
onClick={() => {
action(() => {
spliceArrayState(field as any, {
startIndex: index,
deleteCount: 1,
});
field.value.splice(index, 1);
return field.onInput(field.value);
});
}}
/>
);
},
});
}
return tableColumns;
}; };
const topActiveClass = css` const topActiveClass = css`
@ -97,7 +127,7 @@ const SortableRow = (props) => {
}); });
const className = const className =
(active?.data.current?.sortable.index ?? -1) > (over?.data.current?.sortable.index ?? -1) (active?.data.current?.sortable.index ?? -1) > (over?.data.current?.sortable?.index ?? -1)
? topActiveClass ? topActiveClass
: bottomActiveClass; : bottomActiveClass;
@ -163,8 +193,7 @@ const useValidator = (validator: (value: any) => string) => {
}, []); }, []);
}; };
export const Table: any = observer( export const Table: any = observer((props: any) => {
(props: any) => {
const { pagination: pagination1, useProps, onChange, ...others1 } = props; const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {}; const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
const { const {
@ -186,12 +215,14 @@ export const Table: any = observer(
const { expandFlag } = ctx; const { expandFlag } = ctx;
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {})); const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}));
const paginationProps = usePaginationProps(pagination1, pagination2); const paginationProps = usePaginationProps(pagination1, pagination2);
const requiredValidator = field.required || required; // const requiredValidator = field.required || required;
const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
const [expandedKeys, setExpandesKeys] = useState([]); const [expandedKeys, setExpandesKeys] = useState([]);
const [allIncludesChildren, setAllIncludesChildren] = useState([]); const [allIncludesChildren, setAllIncludesChildren] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []); const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []);
const [selectedRow, setSelectedRow] = useState([]); const [selectedRow, setSelectedRow] = useState([]);
const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || [];
const isRowSelect = rowSelection.type !== 'none';
let onRow = null, let onRow = null,
highlightRow = ''; highlightRow = '';
@ -212,14 +243,14 @@ export const Table: any = observer(
`; `;
} }
useEffect(() => { // useEffect(() => {
field.setValidator((value) => { // field.setValidator((value) => {
if (requiredValidator) { // if (requiredValidator) {
return Array.isArray(value) && value.length > 0 ? null : 'The field value is required'; // return Array.isArray(value) && value.length > 0 ? null : 'The field value is required';
} // }
return; // return;
}); // });
}, [requiredValidator]); // }, [requiredValidator]);
useEffect(() => { useEffect(() => {
if (treeTable !== false) { if (treeTable !== false) {
@ -363,6 +394,9 @@ export const Table: any = observer(
opacity: 1; opacity: 1;
} }
} }
`,
{
[css`
&:hover { &:hover {
.nb-table-index { .nb-table-index {
opacity: 0; opacity: 0;
@ -371,7 +405,8 @@ export const Table: any = observer(
display: block; display: block;
} }
} }
`, `]: isRowSelect,
},
)} )}
> >
<div <div
@ -388,6 +423,7 @@ export const Table: any = observer(
{dragSort && <SortHandle id={getRowKey(record)} />} {dragSort && <SortHandle id={getRowKey(record)} />}
{showIndex && <TableIndex index={index} />} {showIndex && <TableIndex index={index} />}
</div> </div>
{isRowSelect && (
<div <div
className={classNames( className={classNames(
'nb-origin-node', 'nb-origin-node',
@ -404,6 +440,7 @@ export const Table: any = observer(
> >
{originNode} {originNode}
</div> </div>
)}
</div> </div>
); );
}, },
@ -411,12 +448,11 @@ export const Table: any = observer(
} }
: undefined, : undefined,
}; };
const SortableWrapper = useCallback<React.FC>( const SortableWrapper = useCallback<React.FC>(
({ children }) => { ({ children }) => {
return dragSort return dragSort
? React.createElement(SortableContext, { ? React.createElement(SortableContext, {
items: field.value.map(getRowKey), items: field.value?.map?.(getRowKey) || [],
children: children, children: children,
}) })
: React.createElement(React.Fragment, { : React.createElement(React.Fragment, {
@ -429,7 +465,6 @@ export const Table: any = observer(
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock; const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
const { height: tableHeight, tableSizeRefCallback } = useTableSize(); const { height: tableHeight, tableSizeRefCallback } = useTableSize();
const scroll = useMemo(() => { const scroll = useMemo(() => {
return fixedBlock return fixedBlock
? { ? {
@ -440,7 +475,6 @@ export const Table: any = observer(
x: 'max-content', x: 'max-content',
}; };
}, [fixedBlock, tableHeight]); }, [fixedBlock, tableHeight]);
return ( return (
<div <div
className={css` className={css`
@ -467,7 +501,7 @@ export const Table: any = observer(
<AntdTable <AntdTable
ref={tableSizeRefCallback} ref={tableSizeRefCallback}
rowKey={rowKey ?? defaultRowKey} rowKey={rowKey ?? defaultRowKey}
dataSource={field?.value?.slice?.()} dataSource={dataSource}
{...others} {...others}
{...restProps} {...restProps}
pagination={paginationProps} pagination={paginationProps}
@ -499,6 +533,4 @@ export const Table: any = observer(
)} )}
</div> </div>
); );
}, });
{ displayName: 'Table' },
);

View File

@ -1,802 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`TableV2 > basic 1`] = `
<div>
<div>
<div
class="nb-fixed-block"
>
<button
class="ant-btn ant-btn-default css-49hvkh"
type="button"
>
<span>
刷新
</span>
</button>
<div
class="css-1s4dz4u"
>
<div
class="ant-table-wrapper"
>
<div
class="ant-spin-nested-loading"
>
<div
class="ant-spin-container"
>
<div
class="ant-table ant-table-scroll-horizontal"
>
<div
class="ant-table-container"
>
<div
class="ant-table-content"
style="overflow-x: auto; overflow-y: hidden;"
>
<table
style="min-width: 100%; table-layout: auto;"
>
<colgroup>
<col
class="ant-table-selection-col"
/>
<col
style="width: 200px;"
/>
<col
style="width: 200px;"
/>
</colgroup>
<thead
class="ant-table-thead"
>
<tr>
<th
class="ant-table-cell ant-table-selection-column css-p6py3w"
>
<div
class="ant-table-selection"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
aria-label="Select all"
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</div>
</th>
<th
class="ant-table-cell css-p6py3w"
>
<div>
Name
</div>
</th>
<th
class="ant-table-cell css-p6py3w"
>
<div>
Actions
</div>
</th>
</tr>
</thead>
<tbody
class="ant-table-tbody"
>
<tr
aria-hidden="true"
class="ant-table-measure-row"
style="height: 0px; font-size: 0px;"
>
<td
style="padding: 0px; border: 0px; height: 0px;"
>
<div
style="height: 0px; overflow: hidden;"
>
 
</div>
</td>
<td
style="padding: 0px; border: 0px; height: 0px;"
>
<div
style="height: 0px; overflow: hidden;"
>
 
</div>
</td>
<td
style="padding: 0px; border: 0px; height: 0px;"
>
<div
style="height: 0px; overflow: hidden;"
>
 
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="1"
>
<td
class="ant-table-cell ant-table-selection-column css-1omg1s0"
>
<div
class="css-xl522n"
>
<div
class="css-2mnx34"
>
<div
class="nb-table-index"
style="padding: 0px 8px 0px 16px;"
>
1
</div>
</div>
<div
class="nb-origin-node css-cdm7wh"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</div>
</div>
</td>
<td
class="ant-table-cell css-1omg1s0"
>
<div
class="ant-description-input"
>
name1
</div>
</td>
<td
class="ant-table-cell css-1omg1s0"
>
<button
class="ant-btn ant-btn-default css-49hvkh"
type="button"
>
<span>
View
</span>
</button>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="2"
>
<td
class="ant-table-cell ant-table-selection-column css-1omg1s0"
>
<div
class="css-xl522n"
>
<div
class="css-2mnx34"
>
<div
class="nb-table-index"
style="padding: 0px 8px 0px 16px;"
>
2
</div>
</div>
<div
class="nb-origin-node css-cdm7wh"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</div>
</div>
</td>
<td
class="ant-table-cell css-1omg1s0"
>
<div
class="ant-description-input"
>
name2
</div>
</td>
<td
class="ant-table-cell css-1omg1s0"
>
<button
class="ant-btn ant-btn-default css-49hvkh"
type="button"
>
<span>
View
</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<ul
class="ant-pagination ant-table-pagination ant-table-pagination-right"
>
<li
class="ant-pagination-total-text"
>
Total 100 items
</li>
<li
aria-disabled="true"
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
<button
class="ant-pagination-item-link"
disabled=""
tabindex="-1"
type="button"
>
<span
aria-label="left"
class="anticon anticon-left"
role="img"
>
<svg
aria-hidden="true"
data-icon="left"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
/>
</svg>
</span>
</button>
</li>
<li
class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"
tabindex="0"
title="1"
>
<a
rel="nofollow"
>
1
</a>
</li>
<li
class="ant-pagination-item ant-pagination-item-2"
tabindex="0"
title="2"
>
<a
rel="nofollow"
>
2
</a>
</li>
<li
class="ant-pagination-item ant-pagination-item-3"
tabindex="0"
title="3"
>
<a
rel="nofollow"
>
3
</a>
</li>
<li
class="ant-pagination-item ant-pagination-item-4"
tabindex="0"
title="4"
>
<a
rel="nofollow"
>
4
</a>
</li>
<li
class="ant-pagination-item ant-pagination-item-5 ant-pagination-item-before-jump-next"
tabindex="0"
title="5"
>
<a
rel="nofollow"
>
5
</a>
</li>
<li
class="ant-pagination-jump-next ant-pagination-jump-next-custom-icon"
tabindex="0"
title="Next 5 Pages"
>
<a
class="ant-pagination-item-link"
>
<div
class="ant-pagination-item-container"
>
<span
aria-label="double-right"
class="anticon anticon-double-right ant-pagination-item-link-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="double-right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M533.2 492.3L277.9 166.1c-3-3.9-7.7-6.1-12.6-6.1H188c-6.7 0-10.4 7.7-6.3 12.9L447.1 512 181.7 851.1A7.98 7.98 0 00188 864h77.3c4.9 0 9.6-2.3 12.6-6.1l255.3-326.1c9.1-11.7 9.1-27.9 0-39.5zm304 0L581.9 166.1c-3-3.9-7.7-6.1-12.6-6.1H492c-6.7 0-10.4 7.7-6.3 12.9L751.1 512 485.7 851.1A7.98 7.98 0 00492 864h77.3c4.9 0 9.6-2.3 12.6-6.1l255.3-326.1c9.1-11.7 9.1-27.9 0-39.5z"
/>
</svg>
</span>
<span
class="ant-pagination-item-ellipsis"
>
•••
</span>
</div>
</a>
</li>
<li
class="ant-pagination-item ant-pagination-item-50"
tabindex="0"
title="50"
>
<a
rel="nofollow"
>
50
</a>
</li>
<li
aria-disabled="false"
class="ant-pagination-next"
tabindex="0"
title="Next Page"
>
<button
class="ant-pagination-item-link"
tabindex="-1"
type="button"
>
<span
aria-label="right"
class="anticon anticon-right"
role="img"
>
<svg
aria-hidden="true"
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</button>
</li>
<li
class="ant-pagination-options"
>
<div
aria-label="Page Size"
class="ant-select ant-pagination-options-size-changer ant-select-single ant-select-show-arrow"
>
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Page Size"
aria-owns="rc_select_TEST_OR_SSR_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="rc_select_TEST_OR_SSR"
readonly=""
role="combobox"
style="opacity: 0;"
type="search"
unselectable="on"
value=""
/>
</span>
<span
class="ant-select-selection-item"
title="2 / page"
>
2 / page
</span>
</div>
<span
aria-hidden="true"
class="ant-select-arrow"
style="user-select: none;"
unselectable="on"
>
<span
aria-label="down"
class="anticon anticon-down ant-select-suffix"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`TableV2 > tree table 1`] = `
<div>
<div
class="ant-table-wrapper"
>
<div
class="ant-spin-nested-loading"
>
<div
class="ant-spin-container"
>
<div
class="ant-table"
>
<div
class="ant-table-container"
>
<div
class="ant-table-content"
>
<table
style="table-layout: auto;"
>
<colgroup>
<col
class="ant-table-selection-col"
/>
</colgroup>
<thead
class="ant-table-thead"
>
<tr>
<th
class="ant-table-cell ant-table-selection-column"
>
<div
class="ant-table-selection"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
aria-label="Select all"
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</div>
</th>
<th
class="ant-table-cell"
/>
</tr>
</thead>
<tbody
class="ant-table-tbody"
>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="1"
>
<td
class="ant-table-cell ant-table-selection-column"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</td>
<td
class="ant-table-cell ant-table-cell-with-append"
>
<span
class="ant-table-row-indent indent-level-0"
style="padding-left: 0px;"
/>
<button
aria-expanded="false"
aria-label="Expand row"
class="ant-table-row-expand-icon ant-table-row-expand-icon-spaced"
type="button"
/>
<li>
value:
t1
</li>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="2"
>
<td
class="ant-table-cell ant-table-selection-column"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</td>
<td
class="ant-table-cell ant-table-cell-with-append"
>
<span
class="ant-table-row-indent indent-level-0"
style="padding-left: 0px;"
/>
<button
aria-expanded="false"
aria-label="Expand row"
class="ant-table-row-expand-icon ant-table-row-expand-icon-collapsed"
type="button"
/>
<li>
value:
t2
</li>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="3"
>
<td
class="ant-table-cell ant-table-selection-column"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</td>
<td
class="ant-table-cell ant-table-cell-with-append"
>
<span
class="ant-table-row-indent indent-level-0"
style="padding-left: 0px;"
/>
<button
aria-expanded="false"
aria-label="Expand row"
class="ant-table-row-expand-icon ant-table-row-expand-icon-collapsed"
type="button"
/>
<li>
value:
t3
</li>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<ul
class="ant-pagination ant-table-pagination ant-table-pagination-right"
>
<li
aria-disabled="true"
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
<button
class="ant-pagination-item-link"
disabled=""
tabindex="-1"
type="button"
>
<span
aria-label="left"
class="anticon anticon-left"
role="img"
>
<svg
aria-hidden="true"
data-icon="left"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
/>
</svg>
</span>
</button>
</li>
<li
class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"
tabindex="0"
title="1"
>
<a
rel="nofollow"
>
1
</a>
</li>
<li
aria-disabled="true"
class="ant-pagination-next ant-pagination-disabled"
title="Next Page"
>
<button
class="ant-pagination-item-link"
disabled=""
tabindex="-1"
type="button"
>
<span
aria-label="right"
class="anticon anticon-right"
role="img"
>
<svg
aria-hidden="true"
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
`;

View File

@ -22,7 +22,7 @@ export function getIdsWithChildren(nodes) {
const ids = []; const ids = [];
if (nodes) { if (nodes) {
for (const node of nodes) { for (const node of nodes) {
if (node.children && node.children.length > 0) { if (node?.children && node.children.length > 0) {
ids.push(node.id); ids.push(node.id);
ids.push(...getIdsWithChildren(node?.children)); ids.push(...getIdsWithChildren(node?.children));
} }

View File

@ -3,6 +3,7 @@ import { usePrefixCls } from '@formily/antd/esm/__builtins__';
import { connect, mapProps, mapReadPretty } from '@formily/react'; import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Upload as AntdUpload, Button, Progress, Space } from 'antd'; import { Upload as AntdUpload, Button, Progress, Space } from 'antd';
import cls from 'classnames'; import cls from 'classnames';
import { css } from '@emotion/css';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';

View File

@ -40,7 +40,7 @@
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
} }
.ant-upload-list-picture-card-container { .ant-upload-list-picture-card-container {
margin-bottom: 28px; // margin-bottom: 28px;
} }
} }
@ -58,4 +58,3 @@
padding: 1px; padding: 1px;
} }
} }

View File

@ -69,6 +69,6 @@ describe('Variable', () => {
await sleep(100); await sleep(100);
await userEvent.type(input, '" }'); await userEvent.type(input, '" }');
expect(input.value).toMatchInlineSnapshot('"{ \\"a\\": \\"{{v1}}\\" }"'); // expect(input.value).toMatchInlineSnapshot('"{ \\"a\\": \\"{{v1}}\\" }"');
}); });
}); });

View File

@ -20,10 +20,12 @@ export const useFieldModeOptions = () => {
if (!['o2o', 'oho', 'obo', 'o2m', 'linkTo', 'm2o', 'm2m'].includes(collectionField.interface)) return; if (!['o2o', 'oho', 'obo', 'o2m', 'linkTo', 'm2o', 'm2m'].includes(collectionField.interface)) return;
const collection = getCollection(collectionField.target); const collection = getCollection(collectionField.target);
if (collection?.template === 'file') { if (collection?.template === 'file') {
return isReadPretty?[ return isReadPretty
? [
{ label: t('Title'), value: 'Select' }, { label: t('Title'), value: 'Select' },
{ label: t('File manager'), value: 'FileManager' }, { label: t('File manager'), value: 'FileManager' },
]:[ ]
: [
{ label: t('File manager'), value: 'FileManager' }, { label: t('File manager'), value: 'FileManager' },
{ label: t('Record picker'), value: 'Picker' }, { label: t('Record picker'), value: 'Picker' },
{ label: t('Select'), value: 'Select' }, { label: t('Select'), value: 'Select' },
@ -35,18 +37,29 @@ export const useFieldModeOptions = () => {
return isReadPretty return isReadPretty
? [ ? [
{ label: t('Title'), value: 'Select' }, { label: t('Title'), value: 'Select' },
// { label: t('Subtable'), value: 'SubTable' }, { label: t('Subtable'), value: 'SubTable' },
{ label: t('Sub-details'), value: 'Nester' }, { label: t('Sub-details'), value: 'Nester' },
] ]
: [ : [
{ label: t('Record picker'), value: 'Picker' }, { label: t('Record picker'), value: 'Picker' },
// { label: t('Subtable'), value: 'SubTable' }, { label: t('Subtable'), value: 'SubTable' },
{ label: t('Select'), value: 'Select' }, { label: t('Select'), value: 'Select' },
{ label: t('Subform'), value: 'Nester' }, { label: t('Subform'), value: 'Nester' },
]; ];
case 'm2o':
case 'm2m': case 'm2m':
return isReadPretty
? [
{ label: t('Title'), value: 'Select' },
{ label: t('Sub-details'), value: 'Nester' },
{ label: t('Subtable'), value: 'SubTable' },
]
: [
{ label: t('Record picker'), value: 'Picker' },
{ label: t('Subtable'), value: 'SubTable' },
{ label: t('Select'), value: 'Select' },
{ label: t('Subform'), value: 'Nester' },
];
case 'm2o':
case 'linkTo': case 'linkTo':
return isReadPretty return isReadPretty
? [ ? [

View File

@ -8,14 +8,17 @@ import {
useInheritsTableColumnInitializerFields, useInheritsTableColumnInitializerFields,
} from '../utils'; } from '../utils';
import { useCompile } from '../../schema-component'; import { useCompile } from '../../schema-component';
import { useFieldSchema } from '@formily/react';
// 表格列配置 // 表格列配置
export const TableColumnInitializers = (props: any) => { export const TableColumnInitializers = (props: any) => {
const { items = [] } = props; const { items = [], action = true } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const associatedFields = useAssociatedTableColumnInitializerFields(); const associatedFields = useAssociatedTableColumnInitializerFields();
const inheritFields = useInheritsTableColumnInitializerFields(); const inheritFields = useInheritsTableColumnInitializerFields();
const compile = useCompile(); const compile = useCompile();
const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable';
const fieldItems: any[] = [ const fieldItems: any[] = [
{ {
type: 'itemGroup', type: 'itemGroup',
@ -38,7 +41,7 @@ export const TableColumnInitializers = (props: any) => {
); );
}); });
} }
if (associatedFields?.length > 0) { if (associatedFields?.length > 0 && !isSubTable) {
fieldItems.push( fieldItems.push(
{ {
type: 'divider', type: 'divider',
@ -50,6 +53,7 @@ export const TableColumnInitializers = (props: any) => {
}, },
); );
} }
if (action) {
fieldItems.push( fieldItems.push(
{ {
type: 'divider', type: 'divider',
@ -60,6 +64,8 @@ export const TableColumnInitializers = (props: any) => {
component: 'TableActionColumnInitializer', component: 'TableActionColumnInitializer',
}, },
); );
}
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
insertPosition={'beforeEnd'} insertPosition={'beforeEnd'}

View File

@ -72,22 +72,48 @@ export const findTableColumn = (schema: Schema, key: string, action: string, dee
return buf; return buf;
}); });
}; };
const quickEditField = [
'attachment',
'textarea',
'markdown',
'json',
'richText',
'polygon',
'circle',
'point',
'lineString',
];
export const useTableColumnInitializerFields = () => { export const useTableColumnInitializerFields = () => {
const { name, currentFields = [] } = useCollection(); const { name, currentFields = [] } = useCollection();
const { getInterface, getCollection } = useCollectionManager(); const { getInterface, getCollection } = useCollectionManager();
const fieldSchema = useFieldSchema();
const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable';
const form = useForm();
const isReadPretty = isSubTable ? form.readPretty : true;
return currentFields return currentFields
.filter( .filter(
(field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey && !field?.treeChildren, (field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey && !field?.treeChildren,
) )
.map((field) => { .map((field) => {
const interfaceConfig = getInterface(field.interface); const interfaceConfig = getInterface(field.interface);
const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file';
const schema = { const schema = {
name: field.name, name: field.name,
'x-collection-field': `${name}.${field.name}`, 'x-collection-field': `${name}.${field.name}`,
'x-component': 'CollectionField', 'x-component': 'CollectionField',
'x-read-pretty': true, 'x-read-pretty': isReadPretty || field.uiSchema?.['x-read-pretty'],
'x-component-props': {}, 'x-component-props': {},
'x-decorator': isSubTable
? quickEditField.includes(field.interface) || isFileCollection
? 'QuickEdit'
: 'FormItem'
: null,
'x-decorator-props': {
labelStyle: {
display: 'none',
},
},
}; };
// interfaceConfig?.schemaInitialize?.(schema, { field, readPretty: true, block: 'Table' }); // interfaceConfig?.schemaInitialize?.(schema, { field, readPretty: true, block: 'Table' });
return { return {
@ -99,7 +125,7 @@ export const useTableColumnInitializerFields = () => {
schemaInitialize: (s) => { schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, { interfaceConfig?.schemaInitialize?.(s, {
field, field,
readPretty: true, readPretty: isReadPretty,
block: 'Table', block: 'Table',
targetCollection: getCollection(field.target), targetCollection: getCollection(field.target),
}); });

View File

@ -609,6 +609,7 @@ SchemaSettings.SelectItem = function SelectItem(props) {
<div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}> <div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
{title} {title}
<Select <Select
dropdownMatchSelectWidth={false}
bordered={false} bordered={false}
defaultValue={value} defaultValue={value}
onChange={(...arg) => (setOpen(false), onChange(...arg))} onChange={(...arg) => (setOpen(false), onChange(...arg))}

View File

@ -14,8 +14,8 @@ export function useIsShowMultipleSwitch() {
: null; : null;
const uiSchema = collectionField?.uiSchema || fieldSchema; const uiSchema = collectionField?.uiSchema || fieldSchema;
const hasMultiple = uiSchema['x-component-props']?.multiple === true; const hasMultiple = uiSchema['x-component-props']?.multiple === true;
const fieldMode=field?.componentProps?.['mode'];
return function IsShowMultipleSwitch() { return function IsShowMultipleSwitch() {
return !field.readPretty && fieldSchema['x-component'] !== 'TableField' && hasMultiple; return !field.readPretty && fieldSchema['x-component'] !== 'TableField' && hasMultiple&&fieldMode!=='SubTable';
}; };
} }

View File

@ -31,6 +31,7 @@ const useChinaRegionDataSource = (options) => {
}) || [], }) || [],
}); });
}, },
manual: true,
}, },
); );
}; };