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

View File

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

View File

@ -46,7 +46,8 @@ const InternalField: React.FC = (props: Props) => {
setFieldProps('title', uiSchema.title);
setFieldProps('description', uiSchema.description);
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'])) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -133,7 +133,7 @@ const InternalFileManager = (props) => {
};
};
return (
<div>
<div style={{ width: '100%', overflow: 'auto' }}>
<FileSelector
value={options}
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}>
<CollectionProvider name={collectionField.target}>
<FormProvider>

View File

@ -1,30 +1,63 @@
import { FormLayout } from '@formily/antd';
import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { css } from '@emotion/css';
import { FormItem, FormLayout } from '@formily/antd';
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
import React, { useEffect } from 'react';
import { CollectionProvider } from '../../../collection-manager';
import { SchemaComponentOptions } from '../../../schema-component';
import Select from '../select/Select';
import { useAssociationFieldContext, useInsertSchema } from './hooks';
import schema from './schema';
export const InternalSubTable = () => {
const field = useField();
const fieldSchema = useFieldSchema();
const insert = useInsertSchema('SubTable');
const { options } = useAssociationFieldContext();
useEffect(() => {
insert(schema.SubTable);
}, []);
return (
<CollectionProvider name={options.target}>
<FormLayout layout={'vertical'}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.SubTable';
}}
/>
</FormLayout>
</CollectionProvider>
);
};
export const InternalSubTable = observer(
() => {
const field: any = useField();
const fieldSchema = useFieldSchema();
const insert = useInsertSchema('SubTable');
const { options } = useAssociationFieldContext();
useEffect(() => {
insert(schema.SubTable);
field.required = fieldSchema['required'];
}, []);
return (
<CollectionProvider name={options.target}>
<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
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.SubTable';
}}
/>
</SchemaComponentOptions>
</FormLayout>
</CollectionProvider>
);
},
{ displayName: 'InternalSubTable' },
);

View File

@ -6,6 +6,7 @@ import { CollectionProvider } from '../../../collection-manager';
import { RecordProvider, useRecord } from '../../../record-provider';
import { FormProvider } from '../../core';
import { useCompile } from '../../hooks';
import { useDesignable } from '../../';
import { ActionContextProvider, useActionContext } from '../action';
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
@ -35,6 +36,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
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>();
@ -53,7 +55,9 @@ export const ReadPrettyInternalViewer: React.FC = observer(
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
insertViewer(schema.Viewer);
if (designable) {
insertViewer(schema.Viewer);
}
setVisible(true);
setRecord(record);
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 { exchangeArrayState } from '@formily/core/esm/shared/internals';
import { observer } from '@formily/react';
import { action } from '@formily/reactive';
import { isArr } from '@formily/shared';
import { Button } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Table } from '../table-v2/Table';
import { useAssociationFieldContext } from './hooks';
export const SubTable: any = observer(
(props) => {
(props: any) => {
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 (
<div>
<Table size={'small'} showIndex field={field} pagination={false} />
{field.editable && (
<Button
type={'dashed'}
block
style={{ marginTop: 12 }}
onClick={() => {
field.value = field.value || [];
field.value.push({});
}}
>
Add new
</Button>
)}
<div
className={css`
.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
type={'text'}
block
className={css`
display: block;
`}
onClick={() => {
field.value = field.value || [];
field.value.push({});
field.onInput(field.value);
}}
icon={<PlusOutlined />}
>
{/* {t('Add new')} */}
</Button>
)
}
/>
</div>
);
},

View File

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

View File

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

View File

@ -72,13 +72,21 @@ const useDataTemplates = () => {
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 }) => {
const { templates, display, enabled, defaultTemplate } = useDataTemplates();
const [value, setValue] = React.useState(defaultTemplate?.key || 'none');
const api = useAPIClient();
const { t } = useTranslation();
useEffect(() => {
if (enabled && defaultTemplate) {
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 {
white-space: pre-wrap;
}

View File

@ -45,4 +45,5 @@ export * from './time-picker';
export * from './tree-select';
export * from './upload';
export * from './variable';
export * from './quick-edit'
import './index.less';

View File

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

View File

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

View File

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

View File

@ -2,8 +2,9 @@ import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCollectionManager } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled } from '../../../schema-settings';
import { useCompile, useDesignable } from '../../hooks';
import { useAssociationFieldContext } from '../association-field/hooks';
const useLabelFields = (collectionName?: any) => {
// 需要在组件顶层调用
@ -26,7 +27,7 @@ const useLabelFields = (collectionName?: any) => {
export const TableColumnDesigner = (props) => {
const { uiSchema, fieldSchema, collectionField } = props;
const { getInterface, getCollection } = useCollectionManager();
const field = useField();
const field: any = useField();
const { t } = useTranslation();
const columnSchema = useFieldSchema();
const { dn } = useDesignable();
@ -36,6 +37,15 @@ export const TableColumnDesigner = (props) => {
const intefaceCfg = getInterface(collectionField?.interface);
const targetCollection = getCollection(collectionField?.target);
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 (
<GeneralSchemaDesigner disableInitializer>
<SchemaSettings.ModalItem
@ -101,7 +111,7 @@ export const TableColumnDesigner = (props) => {
dn.refresh();
}}
/>
{intefaceCfg && intefaceCfg.sortable === true && (
{intefaceCfg && intefaceCfg.sortable === true && !currentMode && (
<SchemaSettings.SwitchItem
title={t('Sortable')}
checked={field.componentProps.sorter}
@ -125,7 +135,8 @@ export const TableColumnDesigner = (props) => {
{['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho', 'snapshot', 'createdBy', 'updatedBy'].includes(
collectionField?.interface,
) &&
!isFileField && (
!isFileField &&
field.readPrety && (
<SchemaSettings.SwitchItem
title={t('Enable link')}
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.Remove
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 { css } from '@emotion/css';
import { ArrayField, Field } from '@formily/core';
import { spliceArrayState } from '@formily/core/esm/shared/internals';
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 { Table as AntdTable, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames';
@ -71,12 +72,41 @@ const useTableColumns = (props) => {
if (!exists) {
return columns;
}
return columns.concat({
const tableColumns = columns.concat({
title: render(),
dataIndex: 'TABLE_COLUMN_INITIALIZER',
key: 'TABLE_COLUMN_INITIALIZER',
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`
@ -97,7 +127,7 @@ const SortableRow = (props) => {
});
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
: bottomActiveClass;
@ -163,165 +193,166 @@ const useValidator = (validator: (value: any) => string) => {
}, []);
};
export const Table: any = observer(
(props: any) => {
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
const {
dragSort = false,
showIndex = true,
onRowSelectionChange,
onChange: onTableChange,
rowSelection,
rowKey,
required,
onExpand,
...others
} = { ...others1, ...others2 } as any;
const field = useArrayField(others);
const columns = useTableColumns(others);
const schema = useFieldSchema();
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
const { expandFlag } = ctx;
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => { }));
const paginationProps = usePaginationProps(pagination1, pagination2);
const requiredValidator = field.required || required;
const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
const [expandedKeys, setExpandesKeys] = useState([]);
const [allIncludesChildren, setAllIncludesChildren] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []);
const [selectedRow, setSelectedRow] = useState([]);
export const Table: any = observer((props: any) => {
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
const {
dragSort = false,
showIndex = true,
onRowSelectionChange,
onChange: onTableChange,
rowSelection,
rowKey,
required,
onExpand,
...others
} = { ...others1, ...others2 } as any;
const field = useArrayField(others);
const columns = useTableColumns(others);
const schema = useFieldSchema();
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
const { expandFlag } = ctx;
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}));
const paginationProps = usePaginationProps(pagination1, pagination2);
// const requiredValidator = field.required || required;
const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
const [expandedKeys, setExpandesKeys] = useState([]);
const [allIncludesChildren, setAllIncludesChildren] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []);
const [selectedRow, setSelectedRow] = useState([]);
const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || [];
const isRowSelect = rowSelection.type !== 'none';
let onRow = null,
highlightRow = '';
let onRow = null,
highlightRow = '';
if (onClickRow) {
onRow = (record) => {
return {
onClick: () => onClickRow(record, setSelectedRow, selectedRow),
};
};
highlightRow = css`
& > td {
background-color: #caedff !important;
}
&:hover > td {
background-color: #caedff !important;
}
`;
}
useEffect(() => {
field.setValidator((value) => {
if (requiredValidator) {
return Array.isArray(value) && value.length > 0 ? null : 'The field value is required';
}
return;
});
}, [requiredValidator]);
useEffect(() => {
if (treeTable !== false) {
const keys = getIdsWithChildren(field.value?.slice?.());
setAllIncludesChildren(keys);
}
}, [field.value]);
useEffect(() => {
if (expandFlag) {
setExpandesKeys(allIncludesChildren);
} else {
setExpandesKeys([]);
}
}, [expandFlag, allIncludesChildren]);
const components = useMemo(() => {
if (onClickRow) {
onRow = (record) => {
return {
header: {
wrapper: (props) => {
return (
<DndContext>
<thead {...props} />
</DndContext>
);
},
cell: (props) => {
return (
<th
{...props}
className={cls(
props.className,
css`
max-width: 300px;
white-space: nowrap;
&:hover .general-schema-designer {
display: block;
}
`,
)}
/>
);
},
},
body: {
wrapper: (props) => {
return (
<DndContext
onDragEnd={(e) => {
if (!e.active || !e.over) {
console.warn('move cancel');
return;
}
onClick: () => onClickRow(record, setSelectedRow, selectedRow),
};
};
highlightRow = css`
& > td {
background-color: #caedff !important;
}
&:hover > td {
background-color: #caedff !important;
}
`;
}
const fromIndex = e.active?.data.current?.sortable?.index;
const toIndex = e.over?.data.current?.sortable?.index;
const from = field.value[fromIndex];
const to = field.value[toIndex];
field.move(fromIndex, toIndex);
onRowDragEnd({ fromIndex, toIndex, from, to });
}}
>
<tbody {...props} />
</DndContext>
);
},
row: (props) => {
return <SortableRow {...props}></SortableRow>;
},
cell: (props) => (
<td
// useEffect(() => {
// field.setValidator((value) => {
// if (requiredValidator) {
// return Array.isArray(value) && value.length > 0 ? null : 'The field value is required';
// }
// return;
// });
// }, [requiredValidator]);
useEffect(() => {
if (treeTable !== false) {
const keys = getIdsWithChildren(field.value?.slice?.());
setAllIncludesChildren(keys);
}
}, [field.value]);
useEffect(() => {
if (expandFlag) {
setExpandesKeys(allIncludesChildren);
} else {
setExpandesKeys([]);
}
}, [expandFlag, allIncludesChildren]);
const components = useMemo(() => {
return {
header: {
wrapper: (props) => {
return (
<DndContext>
<thead {...props} />
</DndContext>
);
},
cell: (props) => {
return (
<th
{...props}
className={classNames(
className={cls(
props.className,
css`
max-width: 300px;
white-space: nowrap;
.nb-read-pretty-input-number {
text-align: right;
&:hover .general-schema-designer {
display: block;
}
`,
)}
/>
),
);
},
};
}, [field, onRowDragEnd, dragSort]);
},
body: {
wrapper: (props) => {
return (
<DndContext
onDragEnd={(e) => {
if (!e.active || !e.over) {
console.warn('move cancel');
return;
}
const defaultRowKey = (record: any) => {
return field.value?.indexOf?.(record);
const fromIndex = e.active?.data.current?.sortable?.index;
const toIndex = e.over?.data.current?.sortable?.index;
const from = field.value[fromIndex];
const to = field.value[toIndex];
field.move(fromIndex, toIndex);
onRowDragEnd({ fromIndex, toIndex, from, to });
}}
>
<tbody {...props} />
</DndContext>
);
},
row: (props) => {
return <SortableRow {...props}></SortableRow>;
},
cell: (props) => (
<td
{...props}
className={classNames(
props.className,
css`
max-width: 300px;
white-space: nowrap;
.nb-read-pretty-input-number {
text-align: right;
}
`,
)}
/>
),
},
};
}, [field, onRowDragEnd, dragSort]);
const getRowKey = (record: any) => {
if (typeof rowKey === 'string') {
return record[rowKey]?.toString();
} else {
return (rowKey ?? defaultRowKey)(record)?.toString();
}
};
const defaultRowKey = (record: any) => {
return field.value?.indexOf?.(record);
};
const restProps = {
rowSelection: rowSelection
? {
const getRowKey = (record: any) => {
if (typeof rowKey === 'string') {
return record[rowKey]?.toString();
} else {
return (rowKey ?? defaultRowKey)(record)?.toString();
}
};
const restProps = {
rowSelection: rowSelection
? {
type: 'checkbox',
selectedRowKeys: selectedRowKeys,
onChange(selectedRowKeys: any[], selectedRows: any[]) {
@ -349,20 +380,23 @@ export const Table: any = observer(
className={classNames(
checked ? 'checked' : null,
css`
position: relative;
display: flex;
float: left;
align-items: center;
justify-content: space-evenly;
padding-right: 8px;
position: relative;
display: flex;
float: left;
align-items: center;
justify-content: space-evenly;
padding-right: 8px;
.nb-table-index {
opacity: 0;
}
&:not(.checked) {
.nb-table-index {
opacity: 0;
}
&:not(.checked) {
.nb-table-index {
opacity: 1;
}
opacity: 1;
}
}
`,
{
[css`
&:hover {
.nb-table-index {
opacity: 0;
@ -371,28 +405,30 @@ export const Table: any = observer(
display: block;
}
}
`,
`]: isRowSelect,
},
)}
>
<div
className={classNames(
checked ? 'checked' : null,
css`
position: relative;
display: flex;
align-items: center;
justify-content: space-evenly;
`,
position: relative;
display: flex;
align-items: center;
justify-content: space-evenly;
`,
)}
>
{dragSort && <SortHandle id={getRowKey(record)} />}
{showIndex && <TableIndex index={index} />}
</div>
<div
className={classNames(
'nb-origin-node',
checked ? 'checked' : null,
css`
{isRowSelect && (
<div
className={classNames(
'nb-origin-node',
checked ? 'checked' : null,
css`
position: absolute;
right: 50%;
transform: translateX(50%);
@ -400,105 +436,101 @@ export const Table: any = observer(
display: none;
}
`,
)}
>
{originNode}
</div>
)}
>
{originNode}
</div>
)}
</div>
);
},
...rowSelection,
}
: undefined,
};
const SortableWrapper = useCallback<React.FC>(
({ children }) => {
return dragSort
? React.createElement(SortableContext, {
items: field.value.map(getRowKey),
: undefined,
};
const SortableWrapper = useCallback<React.FC>(
({ children }) => {
return dragSort
? React.createElement(SortableContext, {
items: field.value?.map?.(getRowKey) || [],
children: children,
})
: React.createElement(React.Fragment, {
: React.createElement(React.Fragment, {
children,
});
},
[field, dragSort],
);
const fieldSchema = useFieldSchema();
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
},
[field, dragSort],
);
const fieldSchema = useFieldSchema();
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
const { height: tableHeight, tableSizeRefCallback } = useTableSize();
const scroll = useMemo(() => {
return fixedBlock
? {
const { height: tableHeight, tableSizeRefCallback } = useTableSize();
const scroll = useMemo(() => {
return fixedBlock
? {
x: 'max-content',
y: tableHeight,
}
: {
: {
x: 'max-content',
};
}, [fixedBlock, tableHeight]);
return (
<div
className={css`
}, [fixedBlock, tableHeight]);
return (
<div
className={css`
height: 100%;
overflow: hidden;
.ant-table-wrapper {
height: 100%;
overflow: hidden;
.ant-table-wrapper {
.ant-spin-nested-loading {
height: 100%;
.ant-spin-nested-loading {
.ant-spin-container {
height: 100%;
.ant-spin-container {
height: 100%;
display: flex;
flex-direction: column;
}
display: flex;
flex-direction: column;
}
}
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`}
>
<SortableWrapper>
<AntdTable
ref={tableSizeRefCallback}
rowKey={rowKey ?? defaultRowKey}
dataSource={field?.value?.slice?.()}
{...others}
{...restProps}
pagination={paginationProps}
components={components}
onChange={(pagination, filters, sorter, extra) => {
onTableChange?.(pagination, filters, sorter, extra);
}}
onRow={onRow}
rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')}
tableLayout={'auto'}
scroll={scroll}
columns={columns}
expandable={{
onExpand: (flag, record) => {
const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i);
setExpandesKeys(newKeys);
onExpand?.(flag, record);
},
expandedRowKeys: expandedKeys,
}}
/>
</SortableWrapper>
{field.errors.length > 0 && (
<div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active">
{field.errors.map((error) => {
return error.messages.map((message) => <div>{message}</div>);
})}
</div>
)}
</div>
);
},
{ displayName: 'Table' },
);
}
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`}
>
<SortableWrapper>
<AntdTable
ref={tableSizeRefCallback}
rowKey={rowKey ?? defaultRowKey}
dataSource={dataSource}
{...others}
{...restProps}
pagination={paginationProps}
components={components}
onChange={(pagination, filters, sorter, extra) => {
onTableChange?.(pagination, filters, sorter, extra);
}}
onRow={onRow}
rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')}
tableLayout={'auto'}
scroll={scroll}
columns={columns}
expandable={{
onExpand: (flag, record) => {
const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i);
setExpandesKeys(newKeys);
onExpand?.(flag, record);
},
expandedRowKeys: expandedKeys,
}}
/>
</SortableWrapper>
{field.errors.length > 0 && (
<div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active">
{field.errors.map((error) => {
return error.messages.map((message) => <div>{message}</div>);
})}
</div>
)}
</div>
);
});

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 = [];
if (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(...getIdsWithChildren(node?.children));
}

View File

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

View File

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

View File

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

View File

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

View File

@ -8,14 +8,17 @@ import {
useInheritsTableColumnInitializerFields,
} from '../utils';
import { useCompile } from '../../schema-component';
import { useFieldSchema } from '@formily/react';
// 表格列配置
export const TableColumnInitializers = (props: any) => {
const { items = [] } = props;
const { items = [], action = true } = props;
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const associatedFields = useAssociatedTableColumnInitializerFields();
const inheritFields = useInheritsTableColumnInitializerFields();
const compile = useCompile();
const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable';
const fieldItems: any[] = [
{
type: 'itemGroup',
@ -33,12 +36,12 @@ export const TableColumnInitializers = (props: any) => {
{
type: 'itemGroup',
title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
children: Object.values(inherit)[0].filter((v)=>!v?.field?.isForeignKey),
children: Object.values(inherit)[0].filter((v) => !v?.field?.isForeignKey),
},
);
});
}
if (associatedFields?.length > 0) {
if (associatedFields?.length > 0 && !isSubTable) {
fieldItems.push(
{
type: 'divider',
@ -50,16 +53,19 @@ export const TableColumnInitializers = (props: any) => {
},
);
}
fieldItems.push(
{
type: 'divider',
},
{
type: 'item',
title: t('Action column'),
component: 'TableActionColumnInitializer',
},
);
if (action) {
fieldItems.push(
{
type: 'divider',
},
{
type: 'item',
title: t('Action column'),
component: 'TableActionColumnInitializer',
},
);
}
return (
<SchemaInitializer.Button
insertPosition={'beforeEnd'}

View File

@ -72,22 +72,48 @@ export const findTableColumn = (schema: Schema, key: string, action: string, dee
return buf;
});
};
const quickEditField = [
'attachment',
'textarea',
'markdown',
'json',
'richText',
'polygon',
'circle',
'point',
'lineString',
];
export const useTableColumnInitializerFields = () => {
const { name, currentFields = [] } = useCollection();
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
.filter(
(field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey && !field?.treeChildren,
)
.map((field) => {
const interfaceConfig = getInterface(field.interface);
const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file';
const schema = {
name: field.name,
'x-collection-field': `${name}.${field.name}`,
'x-component': 'CollectionField',
'x-read-pretty': true,
'x-read-pretty': isReadPretty || field.uiSchema?.['x-read-pretty'],
'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' });
return {
@ -99,7 +125,7 @@ export const useTableColumnInitializerFields = () => {
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
readPretty: true,
readPretty: isReadPretty,
block: 'Table',
targetCollection: getCollection(field.target),
});

View File

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

View File

@ -14,8 +14,8 @@ export function useIsShowMultipleSwitch() {
: null;
const uiSchema = collectionField?.uiSchema || fieldSchema;
const hasMultiple = uiSchema['x-component-props']?.multiple === true;
const fieldMode=field?.componentProps?.['mode'];
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,
},
);
};